Monthly Archives: January 2015

WPF TimeLine Custom Control

TimeLine2

 

This is a custom WPF control and not a UserForm or a standard control with some extra properties in XAML. It is a proper class, which you would usually place in a library.

All Controls inherit from the FrameworkElement class. Some inherit from the Control Class, which is derived from the FrameworkElement class. In this case here I am deriving our custom control from the Canvas class, which in turn is derived from the Panel class and inherently FrameworkElement class. Once again I am not re-inventing the wheel and use existing functionality. Therefore the code remains pretty short. I am basically overriding the OnRender() event of the Canvas class.

This effects the design-time and run-time, which means you can see immediate results while you add the control in the XAML/Blend editor and of course when your C# application is running.
Just in case you are interested in further reading for design-time enhancements: WpfTutorial

The code in the MainWindow is not required. This is just a demonstration on how to change the display at run-time. A timer is used to make the change obvious. The code execution would be too fast otherwise. You wouldn’t see the first TimeLine graph, just the second. The method SetTimeSeries() triggers the change by invalidating the Canvas control (method InvalidateVisual()).

You can see a similar initialization in the TimeLineControl constructor. This data can be seen at design-time. You could also code that part in the XAML code, but this is not the purpose here. The control should not need any additional XAML in its ‘distributable’ version. Implement the TimeLineControl as it is. That is the beauty of overriding OnRender() rather than using WPF templates.

Design-time data display:
TimeLine1

The inner control paddings and other settings were hard-coded. You can change these. Hard-coding made the example easier to understand – no redundant code to confuse you. Feel free to replace this part with flexible user settings or calculated values.

 

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="TimeLine.MainWindow"
        xmlns:local="clr-namespace:TimeLine"
  Title="TimeLineControl" Height="130" Width="525">
  <DockPanel>
    <local:TimeLineControl x:Name="MyTimeLineControl" />
  </DockPanel>
</Window>

 

using System;
using System.Collections.Generic;
using System.Timers;
using System.Windows;
using System.Windows.Documents;

namespace TimeLine {

  public partial class MainWindow : Window {
    public MainWindow() {
      InitializeComponent();

      Timer lTimer = new Timer(3000.0);
      lTimer.Elapsed += Timer_Elapsed;
      lTimer.Start();
    } // constructor

    void Timer_Elapsed(object xSender, ElapsedEventArgs e) {
      Timer lTimer = xSender as Timer;
      if (lTimer == null) return;
      lTimer.Stop();

      // demo: how to change the TimeLine
      List<TimeEvent> lList = new List<TimeEvent>();
      AddEvent(lList, new DateTime(2015, 03, 01), "");
      AddEvent(lList, new DateTime(2015, 03, 06), "exD Brown-Forman Corp");
      AddEvent(lList, new DateTime(2015, 03, 10), "exD UniFirst Corp");
      AddEvent(lList, new DateTime(2015, 03, 11), "exD Worthington Industries Inc");
      AddEvent(lList, new DateTime(2015, 03, 12), "exD Garmin Ltd");
      AddEvent(lList, new DateTime(2015, 03, 18), "exD Republic Bank Corp");
      AddEvent(lList, new DateTime(2015, 03, 23), "exD STMicroelectronics NV");
      AddEvent(lList, new DateTime(2015, 03, 31), "");
      MyTimeLineControl.SetTimeSeries(lList);
    } // constructor

    private void AddEvent(List<TimeEvent> xList, DateTime xDate, string xText) {
      TimeEvent lEvent = new TimeEvent();
      lEvent.Date = xDate;
      lEvent.TextRed = xDate.ToString("dd MMMyy");
      lEvent.TextBlack = xText;
      xList.Add(lEvent);
    } //

  } // class
} // namespace

 

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace TimeLine {
  public class TimeLineControl : Canvas {

    private IEnumerable<TimeEvent> _TimeSeries;

    public TimeLineControl() {
      Background = Brushes.White;
      List<TimeEvent> lList = new List<TimeEvent>();
      lList.Add(new TimeEvent { Date = DateTime.Today, TextRed = ":(", TextBlack = "Today" });
      lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(1), TextRed = "", TextBlack = "Tomorrow" });
      lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(7), TextRed = "", TextBlack = "Next week" });
      lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(14), TextRed = "", TextBlack = "Fortnight" });
      lList.Add(new TimeEvent { Date = DateTime.Today.AddMonths(1), TextRed = ":)", TextBlack = "NextMonth" });
      _TimeSeries = lList;
    } // constructor

    public void SetTimeSeries(IEnumerable<TimeEvent> xList) {
      if (!Dispatcher.CheckAccess()) {
        Dispatcher.Invoke(() => SetTimeSeries(xList));
        return;
      }
      _TimeSeries = xList;
      InvalidateVisual();
    } //

    // some hardcoding to keep the example code simple
    const double cPaddingLeft = 15.0;
    const double cPaddingRight = 50.0;
    const double cPaddingBottom = 17.0;
    const double cMarkerLength = 10.0;
    const double cTextOffset = 5.0;   // distance to rotation point
    const double cTextSpace = 7.0;    // distance between the red and the black text
    const double cAngle = 45.0;       // rotation angle  (0.0 <= cAngle <= 180.0)

    protected override void OnRender(DrawingContext xDrawingContext) {
      base.OnRender(xDrawingContext);

      double lLeft = cPaddingLeft;
      double lRight = ActualWidth - cPaddingRight;
      double lBottom = ActualHeight - cPaddingBottom;
      double lWidth = ActualWidth - cPaddingLeft - cPaddingRight;

      Point p1 = new Point(lLeft, lBottom);
      Point p2 = new Point(lRight, lBottom);

      // draw the X-Axis
      Pen lPen = new Pen(Brushes.Black, 3.0);
      lPen.DashStyle = DashStyles.Solid;
      xDrawingContext.DrawLine(lPen, p1, p2);

      // determine range
      DateTime lMin = _TimeSeries.Min(x => x.Date);
      DateTime lMax = _TimeSeries.Max(x => x.Date);
      double lDateRange = lMax.Subtract(lMin).TotalDays;

      foreach (TimeEvent t in _TimeSeries) {
        double lRelativeX = t.Date.Subtract(lMin).TotalDays / lDateRange;
        double lAbsoluteX = lRelativeX * lWidth + lLeft;   // convert to canvas coordinates

        // draw the X-Axis marker
        p1 = new Point(lAbsoluteX, lBottom);
        p2 = new Point(lAbsoluteX, lBottom - cMarkerLength);
        xDrawingContext.DrawLine(lPen, p1, p2);

        // write the text with a 45 degrees angle
        Point lRotationCenter = p2;
        double lTextWidth = DrawText(xDrawingContext, t.TextRed, lRotationCenter, cTextOffset, Brushes.Red); // red text
        DrawText(xDrawingContext, t.TextBlack, lRotationCenter, lTextWidth + cTextOffset + cTextSpace, Brushes.Black); // black text
      }
    } //

    /// <returns>the width of the text</returns>
    private double DrawText(DrawingContext xDrawingContext, string xText, Point xPoint, double xOffset, SolidColorBrush xBrush) {
      Typeface lTypeface = new Typeface("Arial");
      CultureInfo lCultureInfo = CultureInfo.CurrentCulture;
      FormattedText lText = new FormattedText(xText, lCultureInfo, FlowDirection.LeftToRight, lTypeface, 10.0, xBrush);
      RotateTransform lRotateTransform = new RotateTransform(-cAngle, xPoint.X, xPoint.Y);
      xDrawingContext.PushTransform(lRotateTransform);
      xDrawingContext.DrawText(lText, new Point(xPoint.X + xOffset, xPoint.Y - lText.Height / 2.0));
      xDrawingContext.Pop();
      //return new Point(xPoint.X + lText.Height / 2.0 * Math.Sin(cAngle) + lText.Width * Math.Cos(cAngle) , xPoint.Y - lText.Width * Math.Sin(cAngle));
      return lText.Width;
    } //

    private void DrawLine(double x1, double x2, double y1, double y2) {
      Line lLine = new Line();
      lLine.Stroke = Brushes.LightSteelBlue;
      lLine.X1 = x1;
      lLine.X2 = x2;
      lLine.Y1 = y1;
      lLine.Y2 = y2;
      lLine.HorizontalAlignment = HorizontalAlignment.Left;
      lLine.VerticalAlignment = VerticalAlignment.Center;
      lLine.StrokeThickness = 2;
      Children.Add(lLine);
    } //

  } // class
} // namespace
using System;

namespace TimeLine {
  public class TimeEvent {
    public DateTime Date;
    public string TextRed;
    public string TextBlack;
  } // class
} // namespace
Advertisements

Clipboard to Text To Speech (TTS)

ClipboardTTSApp

 

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.

 

SystemSpeech

 

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.
http://msdn.microsoft.com/en-us/library/ee125077%28v=vs.85%29.aspx

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"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ClipboardTTS" Height="140" Width="263"
        Topmost="True"
        Loaded="Window_Loaded"
        Closed="Window_Closed">
  <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"/>
    </DockPanel>
    <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 !
    </TextBox>
  </DockPanel>
</Window>

 

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() {
      InitializeComponent();
    } // constructor

    private void StartListeningToClipboard() {
      WindowInteropHelper lWindowInteropHelper = new WindowInteropHelper(this);
      _HWndSource = HwndSource.FromHwnd(lWindowInteropHelper.Handle);
      _HWndSource.AddHook(WinProc);
      _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;
      _HWndSource.RemoveHook(WinProc);
    } //

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

    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);
          break;

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

          processWinProcMessage();
          break;
      }

      return IntPtr.Zero;
    } //

    private void processWinProcMessage() {
      if (!Dispatcher.CheckAccess()) {
        Dispatcher.Invoke(processWinProcMessage);
        return;
      }

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

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

    private void Button_SayIt_Click(object xSender, RoutedEventArgs e) {
      if (_SpeechSynthesizer.State == SynthesizerState.Speaking) {
        _SpeechSynthesizer.SpeakAsyncCancelAll();
        return;
      }
      SayIt(TextBox_Clipboard.Text);
    } //

    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.Items.Add(xVoice.VoiceInfo.Name);
      }
      ComboBox_Voices.SelectedIndex = 0;

      StartListeningToClipboard();
    } //

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

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

    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;
      lDialog.ShowDialog();
    } //

    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));
        return;
      }

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

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

  } // class
} // namespace