Blog Archives

Clipboard to Text To Speech (TTS)



Since two years I am using Text To Speech (TTS). The quality has improved a lot. The spoken text can be understood and the pronunciations have reached good levels; not perfect though. Since Windows 7 there is no need to pay for a professional voice anymore. The Microsoft Windows system voices are sufficient.
First, I wanted to build my own add-on for the Firefox browser. But I quickly realised that there are too many constraints. I am using a nice tool on my Samsung Note 4 to listen to web site texts on a daily basis. That works out very well.

Nevertheless, this tool here is for home PCs.
ClipboardTTS monitors the windows clipboard and displays the current text in a WPF TextBox. In case the CheckBox “Auto” is checked the application starts speaking the text immediately. You can also generate WAV files. Adding this feature only took a few extra lines, otherwise it would not have been worth it. There are only a few use cases.

The Clipboard class offered in .Net does not provide events to monitor clipboard changes. We therefore have to use the old fashioned 32bit Windows functions. Therefore the codes section starts with imports from the windows “user32.dll”. With these you can subscribe to updates. The event WinProc notifies the application about changes. Most of these messages are disregarded in this application. We are only interested in two types of them. The first one is the Windows clipboard chain WM_CHANGECBCHAIN. You have to store the following window of the system clipboard chain, because we must forward messages to that one. This is a weird technology, but who knows what it is good for. For sure it simplifies suppressing messages without the need for any cancellation flag.
WM_DRAWCLIPBOARD is the other type we are interested in. This message tells you that the clipboard context has changed. Have a look at the C# code, you’ll quickly understand.
You could argue that the .Net Clipboard class is not needed, after all the user32.dll can do everything we need. Well, I think we should include as much .Net as possible. This is the right way to stick to the future.

Don’t forget to reference the System.Speech library in Visual Studio.




The application itself is pretty short. This is due to two facts. Windows is offering good SAPI voices and acceptable .Net support to use these voices.

I don’t see the point to implement an add-on for Firefox to follow the multi-platform approach. Do I have to re-invent the wheel? And check out the existing add-ons. You can hardly understand the spoken text. Some of these add-ons offer multi-language support. Yeah, but come on! You cannot understand a word. We are not in the 1990s anymore. Computers have learnt speaking very well. What is the language support good for if you cannot understand anything?


<Window x:Class="ClipboardTTS.MainWindow"
        Title="ClipboardTTS" Height="140" Width="263"
  <DockPanel LastChildFill="True">
    <DockPanel DockPanel.Dock="Top" LastChildFill="False">
      <CheckBox Name="Checkbox_OnOff" DockPanel.Dock="Left" Content="Auto" Margin="5" ToolTip="Speak as soon as the clipboard text changes"/>
      <Button Content="Say it"  DockPanel.Dock="Right" Click="Button_SayIt_Click" Width="50" Margin="5" ToolTip="Start/Stop speaking"/>
      <Button Name="Button_Save" Content="Save"  DockPanel.Dock="Right" Click="Button_Save_Click" Width="50" Margin="5" ToolTip="Create WAV sound file"/>
    <ComboBox Name="ComboBox_Voices" DockPanel.Dock="Top"  SelectionChanged="ComboBox_Voices_SelectionChanged" ToolTip="Voice"/>
    <Slider Name="Slider_Volumne" DockPanel.Dock="Top" Minimum="0" Maximum="100" Value="50" ValueChanged="Slider_Volumne_ValueChanged" ToolTip="Volume" />
    <TextBox Name="TextBox_Clipboard" TextChanged="TextBox_Clipboard_TextChanged" >
      hello world !


using System;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using System.Speech.Synthesis;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;

namespace ClipboardTTS {
  public partial class MainWindow : Window {

    private const int WM_DRAWCLIPBOARD = 0x0308; // change notifications
    private const int WM_CHANGECBCHAIN = 0x030D; // another window is removed from the clipboard viewer chain
    private const int WM_CLIPBOARDUPDATE = 0x031D; // clipboard changed contents

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetClipboardViewer(IntPtr xHWndNewViewer);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool ChangeClipboardChain(IntPtr xHWndRemove, IntPtr xHWndNewNext);

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SendMessage(IntPtr xHWnd, int xMessage, IntPtr xWParam, IntPtr xLParam);

    private IntPtr _HWndNextViewer; // next window
    private HwndSource _HWndSource; // this window
    private string _Text = string.Empty;
    private SpeechSynthesizer _SpeechSynthesizer = new SpeechSynthesizer();

    public MainWindow() {
    } // constructor

    private void StartListeningToClipboard() {
      WindowInteropHelper lWindowInteropHelper = new WindowInteropHelper(this);
      _HWndSource = HwndSource.FromHwnd(lWindowInteropHelper.Handle);
      _HWndNextViewer = SetClipboardViewer(_HWndSource.Handle);   // set this window as a viewer
    } //

    private void StopListeningToClipboard() {
      ChangeClipboardChain(_HWndSource.Handle, _HWndNextViewer); // remove from cliboard viewer chain
      _HWndNextViewer = IntPtr.Zero;
    } //

    private void SayIt(string xText) {
      if (string.IsNullOrWhiteSpace(xText)) return;
      _SpeechSynthesizer.Volume = (int)Slider_Volumne.Value;
    } //

    private IntPtr WinProc(IntPtr xHwnd, int xMessageType, IntPtr xWParam, IntPtr xLParam, ref bool xHandled) {
      switch (xMessageType) {
        case WM_CHANGECBCHAIN:
          if (xWParam == _HWndNextViewer) _HWndNextViewer = xLParam;
          else if (_HWndNextViewer != IntPtr.Zero) SendMessage(_HWndNextViewer, xMessageType, xWParam, xLParam);

        case WM_DRAWCLIPBOARD:
          SendMessage(_HWndNextViewer, xMessageType, xWParam, xLParam);


      return IntPtr.Zero;
    } //

    private void processWinProcMessage() {
      if (!Dispatcher.CheckAccess()) {

      if (!Clipboard.ContainsText()) return;
      string lPreviousText = _Text;
      _Text = Clipboard.GetText();
      if (_Text.Equals(lPreviousText)) return; // do not play the same text again
      if (Checkbox_OnOff.IsChecked.Value) SayIt(_Text);
    } //

    private void InsertTextIntoTextBox(string xText) {
      if (!TextBox_Clipboard.Dispatcher.CheckAccess()) {
        TextBox_Clipboard.Dispatcher.Invoke(() => InsertTextIntoTextBox(xText));
      TextBox_Clipboard.Text = xText;
    } //

    private void Button_SayIt_Click(object xSender, RoutedEventArgs e) {
      if (_SpeechSynthesizer.State == SynthesizerState.Speaking) {
    } //

    private void TextBox_Clipboard_TextChanged(object xSender, System.Windows.Controls.TextChangedEventArgs e) {
      _Text = TextBox_Clipboard.Text;
    } //

    private void Window_Loaded(object xSender, RoutedEventArgs e) {
      ReadOnlyCollection<InstalledVoice> lVoices = _SpeechSynthesizer.GetInstalledVoices();
      if (lVoices.Count < 1) return;

      foreach (InstalledVoice xVoice in lVoices) {
      ComboBox_Voices.SelectedIndex = 0;

    } //

    private void Window_Closed(object xSender, EventArgs e) {
    } //

    private void ComboBox_Voices_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) {
      string xVoice = ComboBox_Voices.SelectedItem as string;
      if (string.IsNullOrWhiteSpace(xVoice)) return;
    } //

    private void Slider_Volumne_ValueChanged(object xSender, RoutedPropertyChangedEventArgs<double> e) {
      _SpeechSynthesizer.Volume = (int)Slider_Volumne.Value;
    } //

    private Brush _OldButtonBrush = SystemColors.ControlBrush;
    private void Button_Save_Click(object xSender, RoutedEventArgs e) {
      _OldButtonBrush = Button_Save.Background;
      Button_Save.Background = Brushes.Salmon;
      Microsoft.Win32.SaveFileDialog lDialog = new Microsoft.Win32.SaveFileDialog();
      string lPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
      lDialog.InitialDirectory = lPath;
      lDialog.FileOk += FileDialog_FileOk;
      lDialog.Filter = "All Files|*.*|WAV (*.wav)|*.wav";
      lDialog.FilterIndex = 2;
    } //

    void FileDialog_FileOk(object xSender, System.ComponentModel.CancelEventArgs e) {
      Microsoft.Win32.SaveFileDialog lDialog = xSender as Microsoft.Win32.SaveFileDialog;
      if (lDialog == null) return;

      if (!Dispatcher.CheckAccess()) {
        Dispatcher.Invoke(() => FileDialog_FileOk(xSender, e));

      try {
        string lPathAndFile = lDialog.FileName;
        _SpeechSynthesizer.SpeakCompleted += SpeechSynthesizer_SpeakCompleted;
        Button_Save.Background = _OldButtonBrush;
      catch (Exception ex) { MessageBox.Show(ex.Message); }
    } //

    void SpeechSynthesizer_SpeakCompleted(object sender, SpeakCompletedEventArgs e) {
      _SpeechSynthesizer.SpeakCompleted -= SpeechSynthesizer_SpeakCompleted;
    } //

  } // class
} // namespace

WPF Datagrid formatting (part 2, advanced)


We stick to the previous DataGrid example and enhance it now.

The improvements/additions are:

  • Cells are vertically centered now.
  • Copy/paste includes the header text.
  • Templates cannot be copied/pasted. The DataGrid does not know what property it has to read. Therefore a ClipboardContentBinding was added.
    ClipboardContentBinding="{Binding Birthday}
  • A yellow smiley is drawn on a Canvas with ellipses and a Bézier curve.
  • The birthday string is formatted.
  • The DataGrid rows use alternating colors.
  • CheckBoxes are centered in the cells.
  • A bit closer to hardcore: DatePicker
    The method to remove all borders requires slightly more know-how. The required information was taken from my earlier post WPF Control Templates (part 1). Also the background color of the DatePickerTextBox is made transparent. This is done without defining a new template for the DatePicker.

    The XAML definition

    <DatePicker SelectedDate="{Binding Birthday, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"  BorderThickness="0" Loaded="DataGrid_DatePicker_Loaded" />


        private void DataGrid_DatePicker_Loaded(object sender, RoutedEventArgs e) {...}

    which in turn calls:

            private static void RemoveBorders(DependencyObject xDependencyObject) {...}
  • RowHeaders were added. On the internet you can find a lot of ToggleButton examples. You face the same code roots over and over again. To avoid coming up with a similar example I used a button with a +/- sign instead. This way you can easily change the code and replace the text by custom images.
  • My advice here is: Play with the FrozenColumnCount property. I am sure you will need it someday.
  • This example uses more templates than the last one.
  • RowDetailsTemplate was added. This enables expanding DataGrid rows to eg. show or enter additional information.
  • UpdateSourceTrigger makes sure you can see DataGridCell changes immediately on the RowDetailsTemplate.
    To achieve this, the class Person needs to inherit INotifyPropertyChanged.
<Window x:Class="WpfDatagrid.MainWindow"
        Title="MainWindow" Height="350" Width="525">


        <!-- DataGrid: header style -->
        <Style TargetType="{x:Type DataGridColumnHeader}">
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="SeparatorBrush" Value="WhiteSmoke" />
            <Setter Property="FontWeight" Value="Bold" />

        <!--DataGrid: vertical/horizontal text alignment -->
        <Style x:Key="AlignRight" TargetType="{x:Type TextBlock}">
            <Setter Property="HorizontalAlignment" Value="Right" />
            <Setter	Property="VerticalAlignment" Value="Center" />
        <Style x:Key="AlignLeft" TargetType="{x:Type TextBlock}">
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter	Property="VerticalAlignment" Value="Center" />

        <!--DataGrid: center the CheckBox -->
        <Style x:Key="AlignCheckBox" TargetType="{x:Type DataGridCell}">
            <Setter Property="Template">
                    <ControlTemplate TargetType="{x:Type DataGridCell}">
                        <Grid Background="{TemplateBinding Background}">
                            <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center" />

        <!-- DataGrid: template for expandable area -->
        <DataTemplate x:Key="TemplateRowDetails">
                <Canvas DockPanel.Dock="Left" Width="60">
                        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                            <GradientStop Color="#FF061246" Offset="0.497"/>
                            <GradientStop Color="#FF7F7FC9" Offset="1"/>
                    <Ellipse Fill="Yellow" Height="50" Width="50" StrokeThickness="2" Stroke="Black" Canvas.Left="5" Canvas.Top="5" />
                    <Ellipse Fill="Black" Height="12" Width="8" Canvas.Left="17" Canvas.Top="20" />
                    <Ellipse Fill="Black" Height="12" Width="8" Canvas.Left="37" Canvas.Top="20" />
                    <Path Stroke="Black" StrokeThickness="3">
                                        <PathFigure StartPoint="15,37">
                                                    <QuadraticBezierSegment Point1="30,52" Point2="45,37" />
                <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold"	Text="{Binding FirstName}" />
                <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold"	Text="{Binding LastName}" />
                <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold"	Text="{Binding Birthday, StringFormat={}{0:dd MMMM yyyy}}" />
                <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold"	Text="{Binding Homepage}" />

        <!-- advice: play with the property FrozenColumnCount -->
        <DataGrid AlternatingRowBackground="PeachPuff" 
                  RowDetailsTemplate="{StaticResource TemplateRowDetails}"
                  ColumnHeaderHeight="{Binding RowHeight, RelativeSource={RelativeSource Self}}">

                <Style TargetType="DataGridRow">
                    <Setter Property="DetailsVisibility" Value="Collapsed" />

            <!-- DataGrid: RowHeader -->
                    <Button Click="DataGridRowHeader_Button_Click" Cursor="Hand" HorizontalAlignment="Center" >
                            <Style TargetType="Button">
                                    <Setter Property="VerticalAlignment" Value="Top" />
                                    <Setter Property="Content" Value="+" />
                                    <Setter Property="FontStretch" Value="UltraExpanded" />
                                    <Setter Property="FontWeight" Value="Bold" />
                                    <Setter Property="Height" Value="20" />
                                    <Setter Property= "Width" Value="20" />
                                    <!-- <Setter Property="Width" Value="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}" /> -->
                                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow}},Path=DetailsVisibility}" Value="Visible">
                                        <Setter Property="Background" Value="Salmon" />
                                        <Setter Property="Content" Value="-" />
                                        <Setter Property="Height" Value="86" />

            <!-- DataGrid: Row color when selected -->
                        <Setter Property="DataGridCell.VerticalContentAlignment" Value="Center" />
                        <Trigger Property="DataGridCell.IsSelected" Value="True">
                            <Setter Property="DataGridCell.Background" Value="SteelBlue" />

                <!-- Column: Alive -->
                <DataGridCheckBoxColumn Header="Alive" Binding="{Binding Alive, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" CellStyle="{StaticResource AlignCheckBox}" />

                <!-- Column: Name -->
                <DataGridTextColumn Header="Name" Binding="{Binding FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" ElementStyle="{StaticResource AlignLeft}" />

                <!-- Column: LastName -->
                <DataGridTextColumn Header="LastName" Binding="{Binding LastName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" ElementStyle="{StaticResource AlignLeft}" />

                <!-- Column: Birthday -->
                <DataGridTemplateColumn Header="Birthday" SortMemberPath="Birthday.Day" ClipboardContentBinding="{Binding Birthday}">
                            <DatePicker SelectedDate="{Binding Birthday, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"  BorderThickness="0" Loaded="DataGrid_DatePicker_Loaded" />

                <!-- Column: Age -->
                <DataGridTextColumn Header="Age" Binding="{Binding Age, StringFormat=N2}" ElementStyle="{StaticResource AlignRight}" IsReadOnly="True" />

                <!-- Column: Homepage -->
                <DataGridHyperlinkColumn Header="Homepage" Binding="{Binding Homepage}" IsReadOnly="True">
                            <EventSetter Event="Hyperlink.Click" Handler="Hyperlink_Clicked"/>
                            <Setter Property="TextBlock.HorizontalAlignment" Value="Left" />
                            <Setter Property="TextBlock.VerticalAlignment" Value="Center" />


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Media;

namespace WpfDatagrid {

  public partial class MainWindow : Window {

    public class Person : INotifyPropertyChanged {
      public event PropertyChangedEventHandler PropertyChanged;
      protected void OnPropertyChanged(string xName) {
        PropertyChangedEventHandler h = PropertyChanged;
        if (h == null) return;
        h(this, new PropertyChangedEventArgs(xName));
      } //

      private bool _Alive;
      public bool Alive { get { return _Alive; } set { _Alive = value; OnPropertyChanged("Alive"); } }

      private string _FirstName;
      public string FirstName { get { return _FirstName; } set { _FirstName = value; OnPropertyChanged("FirstName"); } }

      private string _LastName;
      public string LastName { get { return _LastName; } set { _LastName = value; OnPropertyChanged("LastName"); } }

      public double Age { get { return DateTime.Now.Subtract(Birthday).TotalDays / 365; } }
      public string Homepage { get; set; }

      private DateTime _Birthday;
      public DateTime Birthday { get { return _Birthday; } set { _Birthday = value; OnPropertyChanged("Birthday"); } }

    } // class

    public MainWindow() {
    } //

    // set the window DataContext
    private void Window_Loaded(object sender, RoutedEventArgs e) {
      List<Person> lPersons = new List<Person>();
      lPersons.Add(new Person() { FirstName = "Liza", LastName = "Minnelli", Birthday = new DateTime(1946, 03, 12), Alive = true, Homepage = "" });
      lPersons.Add(new Person() { FirstName = "Bastian", LastName = "Ohta", Birthday = new DateTime(1975, 03, 13), Alive = true, Homepage = "" });
      lPersons.Add(new Person() { FirstName = "Albert", LastName = "Einstein", Birthday = new DateTime(1879, 03, 14), Alive = false, Homepage = "" });
      lPersons.Add(new Person() { FirstName = "Coenraad", LastName = "van Houten", Birthday = new DateTime(1801, 03, 15), Alive = false, Homepage = "" });
      lPersons.Add(new Person() { FirstName = "Andrew", LastName = "Miller-Jones", Birthday = new DateTime(1910, 03, 16), Alive = false, Homepage = "dead as a Dodo" });
      lPersons.Add(new Person() { FirstName = "Gottlieb", LastName = "Daimler", Birthday = new DateTime(1834, 03, 17), Alive = false, Homepage = "" });
      lPersons.Add(new Person() { FirstName = "Rudolf", LastName = "Diesel", Birthday = new DateTime(1858, 03, 18), Alive = false, Homepage = "" });
      DataContext = lPersons;
    } //

    // exit the application
    private void Window_Closed(object sender, EventArgs e) {
    } //

    // open the hyperlink in a browser
    private void Hyperlink_Clicked(object sender, RoutedEventArgs e) {
      try {
        Hyperlink lHyperlink = e.OriginalSource as Hyperlink;
        string lUri = lHyperlink.NavigateUri.OriginalString;
      catch (Exception ex) { MessageBox.Show(ex.Message); }
    } //

    // find the correct DataGridRow and set the DetailsVisibility
    private void DataGridRowHeader_Button_Click(object sender, RoutedEventArgs e) {
      DependencyObject lDependencyObject = e.OriginalSource as DependencyObject;
      //Button lButton = lDependencyObject as Button;      
      //if (lButton == null) return;            
      while (!(lDependencyObject is DataGridRow) && lDependencyObject != null) lDependencyObject = VisualTreeHelper.GetParent(lDependencyObject);
      DataGridRow lRow = lDependencyObject as DataGridRow;
      if (lRow == null) return;
      //lRow.IsSelected = (lRow.DetailsVisibility != Visibility.Visible);            
      lRow.DetailsVisibility = lRow.DetailsVisibility == System.Windows.Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed;
    } //

    private void DataGrid_DatePicker_Loaded(object sender, RoutedEventArgs e) {
      // get the DatePicker control
      DatePicker lDatePicker = sender as DatePicker;
      lDatePicker.VerticalContentAlignment = System.Windows.VerticalAlignment.Center;

      // find the inner textbox and adjust the Background colour
      DatePickerTextBox lInnerTextBox = lDatePicker.Template.FindName("PART_TextBox", lDatePicker) as DatePickerTextBox;
      lInnerTextBox.Background = Brushes.Transparent;
      lInnerTextBox.VerticalContentAlignment = System.Windows.VerticalAlignment.Center;
      lInnerTextBox.Height = lDatePicker.ActualHeight - 2;

      // remove watermark
      ContentControl lWatermark = lInnerTextBox.Template.FindName("PART_Watermark", lInnerTextBox) as ContentControl;
      lWatermark.IsHitTestVisible = false;
      lWatermark.Focusable = false;
      lWatermark.Visibility = System.Windows.Visibility.Collapsed;
      lWatermark.Opacity = 0;

      // just as demo
      ContentControl lContentHost = lInnerTextBox.Template.FindName("PART_ContentHost", lInnerTextBox) as ContentControl;

      // remove ugly borders
      RemoveBorders(lInnerTextBox);  // hardcore 🙂      
    } //

    private static void RemoveBorders(DependencyObject xDependencyObject) {
      for (int i = 0, n = VisualTreeHelper.GetChildrenCount(xDependencyObject); i < n; i++) {
        DependencyObject lDependencyObject = VisualTreeHelper.GetChild(xDependencyObject, i);
        Border lBorder = lDependencyObject as Border;
        if (lBorder == null) continue;
        lBorder.BorderBrush = Brushes.Transparent;
    } //

  } // class
} // namespace