news 2026/7/29 5:31:49

WPF字体获取全攻略:三种方案对比与实战数据绑定

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WPF字体获取全攻略:三种方案对比与实战数据绑定

1. 项目概述与核心价值

在桌面应用开发,尤其是WPF项目中,处理字体是一个看似基础却极易踩坑的环节。无论是制作一个支持自定义主题的文本编辑器,还是开发一个需要动态生成报告或预览文档的办公软件,获取系统已安装的字体列表都是第一步。很多开发者,包括我自己在早期,都曾简单地认为调用System.Drawing.FontFamily.Families就能一劳永逸,结果在跨线程、跨平台或者特定系统权限下遇到了各种奇怪的问题。这个项目的核心,就是深入探讨在 C# WPF 环境下,如何可靠、高效、安全地获取计算机上所有已安装字体的名称集合。这不仅仅是调用一个API那么简单,它涉及到不同.NET版本的兼容性、WPF与WinForms的交互、系统目录的访问权限,以及如何优雅地处理结果并绑定到UI上。对于正在构建需要字体选择功能的上位机软件、工业控制界面或者任何注重用户体验的WPF应用开发者来说,掌握一套稳健的字体枚举方案,是提升应用专业度和稳定性的关键一步。

2. 技术方案选型与深度解析

面对“获取已安装字体”这个需求,我们至少有三种主流的.NET技术路径可选。选择哪一种,取决于你的应用目标框架、对性能和安全性的要求,以及是否愿意引入额外的依赖。

2.1 方案一:使用 System.Drawing (GDI+)

这是最经典、最广为人知的方法,源自WinForms时代。其核心是System.Drawing.FontFamily.Families这个静态属性。

原理与实现:

using System.Drawing; using System.Linq; public List<string> GetFontsByGdiPlus() { var fontFamilies = FontFamily.Families; return fontFamilies.Select(f => f.Name).ToList(); }

这段代码简洁明了,在大部分Windows桌面环境下都能工作。它的原理是查询Windows的GDI+字体子系统,该系统维护着系统全局和当前用户会话安装的字体列表。

为什么它可能不是WPF的最佳选择?

  1. 线程亲和性System.Drawing命名空间下的许多类型,包括FontFamily,对线程上下文有要求。它们通常期望在创建了UI线程同步上下文的线程上使用(例如主UI线程)。如果你在一个后台工作线程或Task中直接调用它,可能会引发跨线程访问异常,或者需要额外的Control.Invoke机制(在纯WPF中并不直接存在),增加了复杂度。
  2. .NET Core/.NET 5+ 的兼容性:在跨平台的.NET Core和后续的.NET 5/6/7/8中,System.Drawing不再是默认包含的库,因为它严重依赖Windows的GDI+。虽然可以通过安装System.Drawing.CommonNuGet包来使用,但这在非Windows操作系统上功能受限或行为不一致,违背了WPF应用通常专注于Windows平台的初衷,但也引入了不必要的包依赖。
  3. WPF的集成度:返回的FontFamily对象是System.Drawing命名空间的,而WPF中使用的字体类是System.Windows.Media.FontFamily。虽然可以转换名称,但直接使用WPF原生方法在数据绑定和资源使用上更为顺畅。

注意:如果你的WPF项目目标框架是.NET Framework 4.x,并且确定只在UI主线程进行字体枚举,这个方法快速有效。但对于追求更现代、更安全线程模型的项目,需要考虑其他方案。

2.2 方案二:使用 WPF 原生 Media.Fonts 属性

这是WPF框架自带的方法,也是最符合WPF设计哲学的选择。

原理与实现:

using System.Windows.Media; using System.Linq; public List<string> GetFontsByWpfMedia() { var fonts = Fonts.SystemFontFamilies; return fonts.Select(f => f.Source).ToList(); }

Fonts.SystemFontFamilies返回一个ICollection<FontFamily>。这里的FontFamily.Source属性通常就是字体的家族名称(如“Microsoft YaHei”)。这个方法直接查询WPF字体系统缓存,该缓存与Windows的DirectWrite字体引擎紧密集成。

优势分析:

  1. 线程安全:根据官方文档和大量实践,Fonts.SystemFontFamilies是线程安全的,你可以在任何线程上调用它,无需担心跨线程问题。这对于在后台线程加载字体列表以保持UI流畅性至关重要。
  2. 原生集成:返回的对象直接就是WPF的FontFamily,可以无缝用于TextBlock.FontFamily等属性绑定,或者添加到ComboBox的项源中,无需任何转换。
  3. 性能:由于是框架内置属性,且可能内部有缓存机制,访问速度通常很快。

潜在考量:它返回的是系统字体家族。一个“字体家族”可能包含多个变体(如“Arial”、“Arial Bold”、“Arial Italic”)。FontFamily.Source通常给出基础家族名。如果你需要列出所有具体的、带有样式的字体名(即“完整字体名”),可能需要进一步处理,这引出了我们的第三个方案。

2.3 方案三:查询 Windows 注册表

这是最底层、最直接的方法,直接读取Windows存储字体信息的注册表键。

原理与实现:Windows将已安装字体的信息记录在注册表的HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts(系统字体)和HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Fonts(用户字体)键下。键名是字体文件显示名,键值是字体文件路径。

using Microsoft.Win32; using System.Collections.Generic; public List<string> GetFontsByRegistry() { List<string> fontNames = new List<string>(); string[] registryPaths = new string[] { @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" // 示例,实际应分别处理HKLM和HKCU }; // 读取本地机器(系统)字体 using (RegistryKey localMachineKey = Registry.LocalMachine.OpenSubKey(registryPaths[0])) { if (localMachineKey != null) { foreach (string fontName in localMachineKey.GetValueNames()) { // 注册表键名可能包含路径和扩展名,需要清理。例如:“Arial (TrueType)” -> “Arial” string cleanName = CleanFontRegistryName(fontName); if (!string.IsNullOrEmpty(cleanName) && !fontNames.Contains(cleanName)) { fontNames.Add(cleanName); } } } } // 读取当前用户字体(如果需要) // using (RegistryKey currentUserKey = Registry.CurrentUser.OpenSubKey(registryPaths[1])) { ... } return fontNames; } private string CleanFontRegistryName(string registryName) { // 简单的清理逻辑:移除尾部的“ (TrueType)”、“ (OpenType)”等括号内容 int indexOfParen = registryName.IndexOf('('); if (indexOfParen > 0) { return registryName.Substring(0, indexOfParen).Trim(); } return registryName.Trim(); }

为什么选择或避免此方案?

  • 优点:提供了最原始的数据,可以获取到非常具体的字体文件名(包含样式信息)。不依赖特定的.NET图形库,理论上是依赖最少的方案。
  • 缺点
    1. 复杂性高:需要手动解析注册表键名,这些键名格式不统一(例如“Arial (TrueType)”, “Times New Roman Bold (TrueType)”),清理逻辑容易出错。
    2. 权限问题:访问HKEY_LOCAL_MACHINE可能需要管理员权限,在某些受限环境下可能失败。
    3. 非托管资源:需要妥善处理RegistryKey对象的释放(使用using语句),否则可能导致资源泄漏。
    4. 与WPF集成度低:获取的是字符串名称,需要手动转换为FontFamily对象,并且无法直接获取字体家族的元数据。

方案选型总结:对于绝大多数WPF项目,方案二(使用Fonts.SystemFontFamilies)是最推荐的选择。它线程安全、与WPF无缝集成、代码简洁,且性能可靠。只有在需要获取注册表中那种包含样式的完整字体名,或者在不方便引用PresentationCore程序集的极特殊场景下,才考虑方案三。方案一可作为在遗留的.NET Framework WPF项目中快速实现的备选,但不建议在新项目中使用。

3. 核心实现与UI数据绑定实战

确定了使用WPF原生的Fonts.SystemFontFamilies作为数据源后,我们将其集成到一个典型的MVVM模式应用中,并实现一个字体选择器。

3.1 创建 ViewModel 与字体列表属性

首先,我们创建一个FontPickerViewModel,它负责加载字体列表并暴露给视图。

// FontPickerViewModel.cs using System.Collections.ObjectModel; using System.Linq; using System.Windows.Media; public class FontPickerViewModel : ObservableObject // 假设你有一个实现了INotifyPropertyChanged的基类 { private ObservableCollection<FontFamily> _availableFonts; private FontFamily _selectedFont; public ObservableCollection<FontFamily> AvailableFonts { get => _availableFonts; private set => SetProperty(ref _availableFonts, value); } public FontFamily SelectedFont { get => _selectedFont; set => SetProperty(ref _selectedFont, value); } public FontPickerViewModel() { LoadFonts(); } private void LoadFonts() { // 关键步骤:获取系统字体家族集合 var systemFonts = Fonts.SystemFontFamilies; // 排序并转换为集合。注意:FontFamily可能没有直接的“友好名称”属性,Source通常可用。 // 我们可以按Source的字母顺序排序。 var sortedFonts = systemFonts.OrderBy(f => f.Source).ToList(); AvailableFonts = new ObservableCollection<FontFamily>(sortedFonts); // 设置一个默认选中字体,例如“微软雅黑” SelectedFont = AvailableFonts.FirstOrDefault(f => f.Source.Contains("Microsoft YaHei")) ?? AvailableFonts.FirstOrDefault(); } }

关键点解析:

  1. 使用ObservableCollection<T>:这是WPF数据绑定的最佳搭档。当字体列表发生变化时(虽然在此场景下很少发生),UI会自动更新。
  2. 在构造函数中加载:对于字体列表这种启动时确定、运行时基本不变的数据,在ViewModel构造时加载是合理的。如果列表很大,可以考虑异步加载。
  3. 排序:系统返回的字体集合通常是无序的。按FontFamily.Source排序可以极大提升用户体验,方便用户查找。
  4. 默认选择:提供一个合理的默认值(如系统默认UI字体或第一个字体)是良好的UX设计。

3.2 在 XAML 中绑定与显示

接下来,在XAML中,我们使用ComboBox来显示和选择字体。这里有一个技巧:为了让ComboBox的下拉项直接显示字体的实际外观,我们需要自定义项模板。

<!-- MainWindow.xaml --> <Window x:Class="FontPickerDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF 字体选择器" Height="350" Width="400"> <Window.DataContext> <local:FontPickerViewModel /> </Window.DataContext> <StackPanel Margin="20"> <TextBlock Text="选择字体:" Margin="0,0,0,10"/> <ComboBox x:Name="FontComboBox" ItemsSource="{Binding AvailableFonts}" SelectedItem="{Binding SelectedFont, Mode=TwoWay}" Height="30" VerticalContentAlignment="Center"> <ComboBox.ItemTemplate> <DataTemplate> <!-- 每个下拉项都是一个TextBlock,使用其绑定的FontFamily来渲染自身 --> <TextBlock Text="{Binding Source}" FontFamily="{Binding}" FontSize="14" Padding="4"/> </DataTemplate> </ComboBox.ItemTemplate> <!-- 下拉框顶部显示区域也应用选中的字体 --> <ComboBox.ItemContainerStyle> <Style TargetType="ComboBoxItem"> <Setter Property="FontFamily" Value="{Binding Source}"/> </Style> </ComboBox.ItemContainerStyle> </ComboBox> <!-- 预览区域 --> <Border BorderBrush="Gray" BorderThickness="1" Margin="0,20,0,0" Padding="10"> <TextBlock Text="这是一段预览文字,用于展示所选字体效果。" FontFamily="{Binding SelectedFont}" FontSize="24" TextWrapping="Wrap" HorizontalAlignment="Center"/> </Border> <TextBlock Text="{Binding SelectedFont.Source, StringFormat='当前选中字体:{0}'}" Margin="0,10,0,0" HorizontalAlignment="Center"/> </StackPanel> </Window>

UI设计精要:

  1. ComboBox.ItemTemplate:这是实现“所见即所得”字体列表的核心。我们为每一个FontFamily对象创建一个TextBlock,并将其FontFamily属性绑定到数据项自身({Binding}),这样每个选项的文字就会用其代表的字体来渲染。
  2. ComboBox.ItemContainerStyle:这个样式确保当鼠标悬停或选中下拉列表中的某一项时,该项的容器(ComboBoxItem)也应用了正确的字体,保持视觉一致性。
  3. 实时预览:下方添加一个TextBlock,将其FontFamily绑定到SelectedFont。这样用户选择字体时,预览区域会立即更新,提供即时反馈。

3.3 处理字体加载与性能优化

当系统安装了大量字体(例如超过1000款)时,初始化ComboBox可能会引起短暂的UI卡顿,因为WPF需要为每一个字体项创建视觉元素并测量布局。

优化策略:

  1. 虚拟化ComboBox默认支持UI虚拟化,但前提是将其ItemsSource设置为一个IEnumerable(我们的ObservableCollection符合)并且不破坏虚拟化容器。确保ComboBox放在一个允许滚动的容器中,并且没有设置固定的高度限制其虚拟化区域。
  2. 异步加载:将字体加载逻辑放到后台线程。由于Fonts.SystemFontFamilies是线程安全的,我们可以轻松实现。
private async void LoadFontsAsync() { // 在后台线程获取字体集合 var fontList = await Task.Run(() => Fonts.SystemFontFamilies.OrderBy(f => f.Source).ToList() ); // 回到UI线程更新集合 Application.Current.Dispatcher.Invoke(() => { AvailableFonts = new ObservableCollection<FontFamily>(fontList); SelectedFont = AvailableFonts.FirstOrDefault(f => f.Source.Contains("Segoe UI")) ?? AvailableFonts.FirstOrDefault(); }); }

在构造函数或窗口加载事件中调用LoadFontsAsync(),可以防止在字体数量巨大时界面冻结。

  1. 分页/懒加载:对于极端情况,可以考虑实现一个自定义的控件,只渲染可视区域内的字体项。但这通常超出了简单字体选择器的需求,ComboBox的虚拟化在绝大多数场景下已足够。

4. 进阶话题与疑难杂症排查

在实际开发中,你可能会遇到一些超出基础用法的问题。这里记录了几个常见坑点及其解决方案。

4.1 字体名称本地化与“回退”问题

你可能会发现,在某些非中文系统上,FontFamily.Source返回的名称是英文的(如“Microsoft YaHei”),而在中文系统上,你更希望显示“微软雅黑”。WPF的FontFamily对象包含一个FamilyNames字典属性,它提供了该字体家族在不同语言下的名称。

// 尝试获取当前UI文化下的字体家族名称 var currentCulture = System.Globalization.CultureInfo.CurrentUICulture; string displayName = null; if (fontFamily.FamilyNames.TryGetValue(currentCulture, out displayName)) { // 找到了本地化名称 } else if (fontFamily.FamilyNames.TryGetValue(System.Globalization.CultureInfo.InvariantCulture, out displayName)) { // 回退到不变文化名称(通常是英文) } else { // 最后回退到Source displayName = fontFamily.Source; }

在自定义的ComboBox.ItemTemplate中,你可以绑定到一个经过处理的“显示名称”属性,而不是直接绑定Source

4.2 特定字体文件缺失或损坏

Fonts.SystemFontFamilies返回的是系统注册的字体家族。有时,字体文件可能被删除或损坏,导致WPF在尝试创建该字体的GlyphTypeface时抛出异常。这通常发生在你不仅想列出名字,还想渲染该字体或获取其度量信息时。

防御性编程:

public IEnumerable<FontFamily> GetSafeSystemFonts() { foreach (var fontFamily in Fonts.SystemFontFamilies) { bool isFontValid = false; try { // 尝试获取一个基本的Typeface来验证字体是否可用 var typeface = new Typeface(fontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); // 如果这一步不抛异常,通常认为字体基本可用 isFontValid = true; } catch (Exception ex) // 可能捕获FileNotFoundException, InvalidOperationException等 { System.Diagnostics.Debug.WriteLine($"字体 {fontFamily.Source} 加载失败: {ex.Message}"); // 记录日志或忽略此字体 } if (isFontValid) { yield return fontFamily; } } }

在加载字体列表时,使用上述方法进行过滤,可以避免在后续使用(特别是动态生成文本或文档时)因字体问题导致程序崩溃。

4.3 用户安装字体与系统字体的区别

Fonts.SystemFontFamilies包含了当前会话下所有可用的字体家族,这既包括安装在C:\Windows\Fonts的系统全局字体,也包括为当前用户安装的字体(通常位于%USERPROFILE%\AppData\Local\Microsoft\Windows\Fonts)。一般来说,你不需要区分它们。但如果你的应用有特殊需求(如只允许用户选择系统自带字体),就需要更细粒度的控制。这时,方案三(查询注册表)结合区分HKLMHKCU就能派上用场。你可以分别读取这两个注册表路径,获取字体文件路径,然后只加载系统字体目录下的那些。

4.4 在非UI线程中创建FontFamily对象

有时,你需要在后台线程处理字体数据(例如,生成包含大量文本和不同字体的报告)。虽然获取字体列表(Fonts.SystemFontFamilies)是线程安全的,但直接使用字体名称字符串在后台线程创建新的FontFamily对象可能不安全

// 错误示例(可能在后台线程引发异常): Task.Run(() => { var myFont = new FontFamily("Arial"); // 这里可能有问题! // ... 使用myFont }); // 正确做法: // 1. 在主UI线程提前获取或创建好需要的FontFamily对象。 // 2. 或者,如果必须在后台使用,确保操作在拥有Dispatcher的线程上执行(对于非UI线程创建的控件上下文较复杂)。 // 更安全的模式是:在UI线程获取字体列表(FontFamily对象),然后将这些对象(它们是可跨线程自由传递的引用)传递给后台任务使用。 // 后台任务只进行逻辑计算,不创建新的依赖UI上下文的WPF对象。

经验法则:WPF中与视觉呈现紧密相关的对象(如Brush,Pen, 特定方式创建的DrawingVisual)通常有线程亲和性。而FontFamily对象本身,一旦被创建(通常是在UI线程通过Fonts.SystemFontFamilies获得),其引用可以被安全地跨线程传递和读取属性。但在非UI线程构造一个新的FontFamily,尤其是在涉及资源查找时,可能会遇到问题。最佳实践是在UI线程完成所有WPF对象的创建工作

5. 扩展应用:构建一个健壮的字体管理工具类

基于以上所有讨论,我们可以封装一个更健壮、功能更完整的字体工具类,供整个项目使用。

// FontUtility.cs using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Windows.Media; public static class FontUtility { /// <summary> /// 获取系统所有可用的、有效的字体家族,并按本地化名称排序。 /// </summary> /// <returns>排序后的字体家族集合。</returns> public static IReadOnlyList<FontFamily> GetAvailableFontFamilies() { var validFonts = new List<FontFamily>(); var currentCulture = CultureInfo.CurrentUICulture; foreach (var fontFamily in Fonts.SystemFontFamilies) { if (IsFontFamilyValid(fontFamily)) { validFonts.Add(fontFamily); } } // 按本地化名称或回退名称排序 return validFonts.OrderBy(f => GetFontFamilyDisplayName(f, currentCulture)).ToList(); } /// <summary> /// 获取字体家族对应当前文化的最佳显示名称。 /// </summary> public static string GetFontFamilyDisplayName(FontFamily fontFamily, CultureInfo culture = null) { culture ??= CultureInfo.CurrentUICulture; string displayName; if (fontFamily.FamilyNames.TryGetValue(culture, out displayName)) { return displayName; } // 尝试回退到父文化或不变文化 if (culture.Parent != null && fontFamily.FamilyNames.TryGetValue(culture.Parent, out displayName)) { return displayName; } if (fontFamily.FamilyNames.TryGetValue(CultureInfo.InvariantCulture, out displayName)) { return displayName; } // 最终回退到Source return fontFamily.Source; } /// <summary> /// 验证字体家族是否可用(文件未损坏)。 /// </summary> private static bool IsFontFamilyValid(FontFamily fontFamily) { try { // 尝试创建一个常规的Typeface,不抛异常则认为基本有效 var typeface = new Typeface(fontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); // 进一步,可以尝试获取一个GlyphTypeface,但这更耗时且可能不必要。 // GlyphTypeface glyphTypeface; // return typeface.TryGetGlyphTypeface(out glyphTypeface); return true; } catch { return false; } } /// <summary> /// 异步获取字体列表(适用于大量字体时的UI响应优化)。 /// </summary> public static async Task<IReadOnlyList<FontFamily>> GetAvailableFontFamiliesAsync() { return await Task.Run(() => GetAvailableFontFamilies()); } }

这个工具类提供了线程安全的获取方法、本地化名称支持、字体有效性验证以及异步加载选项。在ViewModel中,你可以这样使用它:

public async Task LoadFontsAsync() { var fonts = await FontUtility.GetAvailableFontFamiliesAsync(); AvailableFonts = new ObservableCollection<FontFamily>(fonts); // ... 设置默认选中 }

通过这样一个从原理到实践,再到优化和封装的完整过程,我们不仅解决了“获取字体名”这个具体问题,更深入理解了WPF字体系统的运作机制、线程安全模型以及如何构建鲁棒的桌面应用功能模块。下次在WPF项目中需要字体选择功能时,你可以自信地选择最合适的方法,并优雅地处理可能出现的边界情况。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/29 5:27:13

FreeRTOS(3):任务挂起与恢复

任务简介任务大体上与上一篇文章的任务差不多&#xff0c;但是将PB1连接的按键用来挂起任务&#xff0c;PB11的按键用来恢复任务&#xff0c;PB4的按键用中断恢复任务&#xff08;使用xTaskResumeFromISR函数&#xff09;。任务创建复制上一篇文章的FreeRTOS_任务创建与删除&am…

作者头像 李华
网站建设 2026/7/29 5:26:33

芯和半导体携手联想集团在DAC 2026现场发布EDA Agent最新研发成果

国产EDA率先落地AI智能体实战应用【美国长滩讯】2026年7月26日&#xff0c;全球规模最大的电子设计自动化&#xff08;EDA&#xff09;行业盛会——DAC 2026在美国加利福尼亚州长滩市开幕。本届大会以“AI For EDA”为核心议题&#xff0c;全球主要EDA厂商均在会上展示AI智能体…

作者头像 李华
网站建设 2026/7/29 5:21:20

STM32 FSMC/FMC外部存储器控制器原理与应用详解

1. 从“为什么需要FSMC/FMC”说起如果你刚开始接触STM32&#xff0c;尤其是那些带屏幕、带SRAM、带NAND Flash的“大”项目&#xff0c;你可能会被一个叫FSMC或者FMC的外设搞得一头雾水。数据手册上那一大堆寄存器、时序图&#xff0c;看起来复杂得让人想放弃。别急&#xff0c…

作者头像 李华
网站建设 2026/7/29 5:20:28

上海系统门窗哪家技术强

随着上海家装及公建市场对系统门窗需求的持续攀升&#xff0c;系统门窗已成为封阳台、阳光房搭建和住宅性能升级的核心品类。上海地处亚热带季风气候区&#xff0c;年降雨量充沛且夏季多台风极端天气&#xff0c;这对门窗的耐候性、防渗水性提出了更高要求。本次推荐的5家系统门…

作者头像 李华
网站建设 2026/7/29 5:20:10

【C++重载操作符与转换】下标操作符

在 C 编程里&#xff0c;操作符重载是一项强大的特性&#xff0c;它能够让我们自定义类的行为&#xff0c;使其操作更符合直觉和业务需求。其中&#xff0c;下标操作符 [] 的重载尤为重要&#xff0c;它使得我们可以像访问数组元素一样访问自定义类对象的元素&#xff0c;极大地…

作者头像 李华
网站建设 2026/7/29 5:17:56

STM32 ADC配置与实战:从原理到多通道采集与滤波优化

1. 项目概述&#xff1a;从模拟到数字的桥梁在嵌入式开发的世界里&#xff0c;我们常常需要和现实世界的物理量打交道&#xff0c;比如温度、压力、光照强度或者电池电压。这些物理量在传感器处理后&#xff0c;通常以连续变化的模拟电压信号形式呈现。然而&#xff0c;我们手中…

作者头像 李华