65.9K
CodeProject 正在变化。 阅读更多。
Home

构建带 RDS 支持的 FM 收音机

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (16投票s)

2009年1月8日

Ms-PL

4分钟阅读

viewsIcon

101119

downloadIcon

3735

本文介绍了如何使用 WPF 和 USBFM 库构建一个带 RDS 支持的简单 FM 收音机播放器。

引言

本文解释了如何使用开源的 USB FM 库 (由 编写) 和 Windows Presentation Foundation 来构建一个简单但功能齐全的具有 RDS 和 TMC 支持的收音机播放器。

背景

USB FM 库提供托管接口,使用 C# 开发,用于 USB FM 接收器,支持 RDS。WPF (Windows Presentation Foundation) 提供了一个易于使用的框架,可以零投入时间地构建丰富的用户界面。将这两者“融合”在一起,将使我们能够构建功能齐全的应用程序,而无需大量的时间投入。

第一步:构建线框图 

为了构建一个 WPF 应用程序,我们应该首先构建线框图。WPF 为我们提供了丰富的布局控件选择。在我们的例子中,我们将使用 Grid 来标记主窗口 (也是唯一窗口) 中的区域。

 <Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="35px"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions> 
 <Grid>

正如您所看到的,我们有三行三列。现在我们可以开始将控件放入其中了。

在任何收音机接收器中,我们都有旋钮来控制音量并调谐到电台。有一个现成的旋钮控件,由 Microsoft Expression Blend 团队准备。那为什么不“按原样”使用它呢?

为了做到这一点,我们必须引用控件库并在应用程序主体的 XAML 文件中定义控件的命名空间。

    xmlns:c="clr-namespace:RotaryControl;assembly=RotaryControl"
...
        <c:RotaryControl Name="Volume" RotationIsConstrained="True"  
		ClockwiseMostAngle="340" Angle="340"/>
        <c:RotaryControl Name="Tune" Grid.Column="2"/>

我们还将添加两个标签和一个预设电台列表框,稍后将与 FM 设备库进行绑定。

         <TextBlock Text="Volume" Grid.Row="1"/>
        <TextBlock Text="Tune" Grid.Row="1" Grid.Column="2"/>

        <ListBox Name="Presets" ItemTemplate="{StaticResource PresetTemplate}" 
		Grid.ColumnSpan="3" Grid.Row="2" Background="Transparent" 
		HorizontalAlignment="Center" >
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <DockPanel Margin="0" IsItemsHost="True"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox> 

在 XAML 标记中唯一剩下的是设置频率和程序文本指示器、单声道/立体声图标和信号强度计的显示。为了设置所有这些,我们将创建另一个网格并将所有内容放入其中。

 <Grid Grid.Column="1">
    <Grid.RowDefinitions>
        <RowDefinition Height="12px"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="20px"/>
        <RowDefinition Height="20px"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width=".2*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <TextBlock Name="Freq" Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" 
		Style="{StaticResource LargeTextStyle}"/>
    <TextBlock Name="PS" Grid.Column="1" Grid.Row="2"/>
    <TextBlock Name="PTY" Grid.Column="1" Grid.Row="0" 
	Style="{StaticResource PTYTextStyle}"/>
    <Path Name="MonoStereo" Stroke="White" Fill="White" 
	Stretch="Fill" Grid.Column="1" Grid.Row="0" Width="12" 
	Height="12" HorizontalAlignment="Left"/>
    <Rectangle Grid.RowSpan="4" Fill="{StaticResource SignalBrush}" Margin="10"/>
    <Rectangle Grid.RowSpan="4" Fill="Black" Margin="9" RenderTransformOrigin="0.5,0">
        <Rectangle.RenderTransform>
            <ScaleTransform x:Name="SignalTransform" ScaleX="1"/>
        </Rectangle.RenderTransform>
    </Rectangle>
    <StackPanel Grid.Column="1" Grid.Row="4" HorizontalAlignment="Right" 
	Orientation="Horizontal">
        <TextBlock Style="{StaticResource IndiStyle}" Text="MS" Name="MS"/>
        <TextBlock Style="{StaticResource IndiStyle}" Text="TA" Name="TA"/>
        <TextBlock Style="{StaticResource IndiStyle}" Text="TP" Name="TP"/>
    </StackPanel>
</Grid> 

我们的应用程序的线框图已完成,现在是时候让它看起来更好了。

第二步:样式化 WPF 应用程序

WPF 不仅易于使用标记构建 UI。它还提供了广泛的样式化可能性。资源具有分层结构,但在我们的应用程序中,我们将所有样式和模板放在 Window.Resource 级别。首先,让我们为所有 TextBlock 设置一个应用程序范围的样式。

<Style TargetType="TextBlock">
    <Setter Property="TextAlignment" Value="Center"/>
    <Setter Property="FontFamily" Value="{x:Static SystemFonts.SmallCaptionFontFamily}"/>
    <Setter Property="FontStyle" Value="{x:Static SystemFonts.SmallCaptionFontStyle}"/>
</Style> 

正如您所看到的,当我们不设置 x:Key 属性时,它会应用于层次结构中较低的所有资源。此外,我们可以继承样式并设置特殊的键来在 XAML 标记和代码中标识资源。

<Style x:Key="LargeTextStyle" TargetType="TextBlock">
    <Setter Property="TextAlignment" Value="Center"/>
    <Setter Property="FontSize" Value="50"/>
</Style>
<Style x:Key="PTYTextStyle" TargetType="TextBlock">
    <Setter Property="TextAlignment" Value="Right"/>
    <Setter Property="FontSize" Value="10"/>
</Style> 

我们还可以使用触发器,它们是直接在样式中的基本事件处理程序

 <Style BasedOn="{StaticResource PTYTextStyle}" x:Key="IndiStyle" TargetType="TextBlock">
    <Setter Property="Margin" Value="5,0,5,0"/>
    <Setter Property="Foreground" Value="White"/>
    <Style.Triggers>
        <Trigger Property="IsEnabled" Value="False">
            <Setter Property="Foreground" Value="Gray"/>
        </Trigger>
    </Style.Triggers>
</Style> 

除了所有这些,我们还可以通过覆盖 Template 属性来完全重新定义控件的外观和感觉,如下所示

<Style TargetType="Button">
    <Setter Property="Foreground" Value="White"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border Height="25" Width="35" BorderThickness=".5" 
			Background="Black" Name="PART_Border" >
                    <Border.BorderBrush>
                        <LinearGradientBrush EndPoint="0.854,0.854" 
			StartPoint="0.146,0.146">
                            <GradientStop Color="#FF262626" Offset="0"/>
                            <GradientStop Color="#FFD7D7D7" Offset="1"/>
                        </LinearGradientBrush>
                    </Border.BorderBrush>
                    <ContentPresenter HorizontalAlignment="Center" 
			SnapsToDevicePixels="True" Margin="0" 
			MouseLeftButtonDown="Button_MouseLeftButtonDown"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsPressed" Value="True">
                        <Setter Property="BorderBrush" TargetName="PART_Border">
                            <Setter.Value>
                                <LinearGradientBrush EndPoint="0.854,0.854" 
				StartPoint="0.146,0.146">
                                    <GradientStop Color="#FF262626" Offset="1"/>
                                    <GradientStop Color="#FFD7D7D7" Offset="0"/>
                                </LinearGradientBrush>
                            </Setter.Value>
                        </Setter>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style> 

但不仅仅是样式可以存储在 Resources 中。我们还可以共享其他对象,例如几何图形 (用于单声道/立体声指示器) 或画笔。

<Geometry x:Key="MonoGeometry">M0,0L1,2 2,2 2,4 1,4 0,6z</Geometry>
<Geometry x:Key="StereoGeometry">M0,0L1,2 2,2 3,0 3,6 2,4 1,4 0,6z</Geometry>
<DrawingBrush x:Key="SignalBrush" TileMode="Tile" Viewport="0,0,.3,.1" Stretch="Uniform">
    <DrawingBrush.Drawing>
        <DrawingGroup>
            <GeometryDrawing Brush="Black">
                <GeometryDrawing.Geometry>
                    <RectangleGeometry Rect="0,0,20,20"/>
                </GeometryDrawing.Geometry>
            </GeometryDrawing>
            <GeometryDrawing Brush="White">
                <GeometryDrawing.Geometry>
                    <RectangleGeometry Rect="0,20,20,40"/>
                </GeometryDrawing.Geometry>
            </GeometryDrawing>
        </DrawingGroup>
    </DrawingBrush.Drawing>
</DrawingBrush> 

我们还可以为应用程序中使用的数据类定义模板。例如,我希望电台预设的 double 值显示为按钮。下面是如何做到这一点

<DataTemplate x:Key="PresetTemplate">
    <Button Content="{Binding}" />
</DataTemplate> 

现在 UI 已完全完成。是时候转向“代码隐藏”了。

第三步:连接基本业务逻辑 

首先,我们必须初始化我们的 USB FM 设备。这是一个非常简单的任务。只需找到它

 _device = USBRadioDevice.FindDevice(RadioPlayer.Properties.Settings.Default.PID, 
	RadioPlayer.Properties.Settings.Default.VID); 

现在我们需要订阅它的事件并为某些成员设置数据绑定

_device.PropertyChanged += (s, ed) => {
   if (ed.PropertyName == "RDS" && _device.RDS != null) { 
      //set bindings
      this.Dispatch(() => {
         Presets.SetBinding(ListBox.ItemsSourceProperty, _device, "Presets");
         Freq.SetBinding(TextBlock.TextProperty, _device, 
		"CurrentFrequency", new ValueConverter<double, 
		double>(d => { return d == 0 ? _device.CurrentStation : d; }));
         PS.SetBinding(TextBlock.TextProperty, _device.RDS, "PS");
         PTY.SetBinding(TextBlock.TextProperty, _device.RDS, "PTYString");
         MonoStereo.SetBinding(Path.DataProperty, _device.RDS, "IsStereo", 
		new ValueConverter<bool, Geometry>(b => 
		{ return (Geometry)(b ? this.Resources["StereoGeometry"] : 
		this.Resources["MonoGeometry"]); }));
         SignalTransform.SetBinding(ScaleTransform.ScaleYProperty, 
		_device.RDS,"SignalStrength", 
		new ValueConverter<byte, double>(b => { return 1-(b / 36d); }));
         MS.SetBinding(TextBlock.IsEnabledProperty, _device.RDS, "HasMS");
         TA.SetBinding(TextBlock.IsEnabledProperty, _device.RDS, "HasTA");
         TP.SetBinding(TextBlock.IsEnabledProperty, _device.RDS, "HasTP");
      });
   }
}; 

在此代码中,我使用了一些我自己开发的“省时”工具来简化某些 WPF 方面的内容。:) 如果您想了解更多关于这些省时工具的信息,请 访问 并通过 RSS 订阅 我的博客。

现在是时候初始化音频和 RDS 报告了

_device.InitAudio();
_device.InitRDSReports(); 

此外,一个小技巧是无需显式设置绑定即可收到音量和调谐旋钮“角度”依赖项属性更改的通知。

Volume.AddValueChanged(RotaryControl.RotaryControl.AngleProperty, (s, ex) => {
   DirectSoundMethods.Volume = (int)Volume.Angle.ToRange
	(Volume.CounterClockwiseMostAngle, Volume.ClockwiseMostAngle, -4000, 0);
});
 Tune.AddValueChanged(RotaryControl.RotaryControl.AngleProperty, (s, ex) => {
   _device.Tune(Tune.Angle > _prevTune);
   _prevTune = Tune.Angle;
}); 

实际上我们已经完成了。现在我们的应用程序几乎准备就绪。剩下的就很清楚和简单了。

第四步:最终化应用程序 

首先,我们在 USB FM 库中使用平台调用,因此它实现了 IDisposable 接口。如果您不想在内存中留下句柄,停止音频、RDS 报告线程并处理 USB 句柄是一个非常好的主意。

  private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
     _device.StopRDSReports();
     _device.StopAudio();
  }
   private void Window_Unloaded(object sender, RoutedEventArgs e) {
     _device.Close();
     _device.Dispose();
     _device = null;
  } 

此外,我们的应用程序运行在无边框窗口中,所以我们必须以某种方式拖动它。为什么不使用 Windows 的 DragMove() 方法来实现呢?请注意,旋钮控件会捕获移动,所以如果我们想让它继续工作,了解鼠标移动事件的来源是有意义的。

private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
    if (e.Source.GetType().IsSubclassOf(typeof(Window)) || 
		e.Source.GetType().Equals(typeof(TextBlock))) DragMove();
} 

最后一件事是获取预设按钮点击并调谐到选定的电台

  private void Button_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
     var button = sender as ContentPresenter;
     if (button != null) {
        var frq = double.Parse(button.Content.ToString());
        _device.Tune(frq);
     }
  } 

我们完成了。现在我们可以编译并运行我们的应用程序了。是不是很有趣?:)

参考文献 

修订历史

  • 2009 年 1 月 8 日 - 发布文章
© . All rights reserved.