czwartek, 24 maja 2012

Program wielowątkowy

Napiszemy prosty program wielowątkowy, zrobimy synchronizację z głównym wątkiem (aby czekał dokładnie do momentu zakończenia drugiego wątku) i dodatkowo prześlemy parametr do nowo tworzonego wątku. To wszystko jest często niezbędne w aplikacjach i warto wiedzieć jak to się robi.

Poprzednio wątki synchronizowane były za pomocą zmiennej bool. Nie jest to dobre rozwiązanie. Dodatkowo wątek główny musiał sprawdzać co pewien czas wartość tej zmiennej. Tutaj rozwiążemy to nieco bardziej optymalnie.

Stwórzmy w nowym konsolowym projekcie nową klasę:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Threads1
{
    class AddParams
    {
        public int a, b;

        public AddParams(int num1, int num2)
        {
            a = num1;
            b = num2;
        }
    }
}

Zwykła klasa posiadająca publiczne pola a i b oraz konstruktor.
Narazie nie wnikamy po co ona jest.
Teraz napiszmy w głownej klasie programu nową metodę statyczną Add:
static void Add(object data)
{
    Console.WriteLine("ID wątku metody Add(): {0}",
        Thread.CurrentThread.ManagedThreadId);

    AddParams ap = (AddParams)data;
    Console.WriteLine("{0} + {1} = {2}",
        ap.a, ap.b, ap.a + ap.b);
}

Ta metoda będzie punktem wejścia wątku, czyli miejscem z którego wątek zacznie swoją pracę. Treść funkcji nie jest skomplikowana. Przyjmuje ona jeden parametr typu object. Ponieważ jest to zwykły obiekt, możemy przesłać do tej metody wsystko. Tak też zrobimy (po to właśnie została utworzona wcześniej klasa AddParams).

Czyli podsumowując:

  • Mamy stworzoną metodę od której nowy wątek zacznie działanie
  • Metoda przyjmuje parametr object który następnie rzutowany jest na wcześniej stworzoną klasę, z której wyciągane są parametry i wykonywane jest dodawanie.

Tworzenie nowego wątku

Dodajmy jeszcze minimalne uśpienie wątku (dla testu). Nie jest ono potrzebne, lecz pomoże zrozumieć istotę synchronizacji.

static void Add(object data)
{
    Console.WriteLine("ID wątku metody Add(): {0}",
        Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(100);
    AddParams ap = (AddParams)data;
    Console.WriteLine("{0} + {1} = {2}",
        ap.a, ap.b, ap.a + ap.b);
}


Utwórzmy teraz nowy wątek. Metoda Main() aplikacji wygląda następująco:

static void Main(string[] args)
{
    AddParams ap = new AddParams(10, 10);
    Thread t = new Thread(new ParameterizedThreadStart(Add));
    t.Start(ap);
    Console.WriteLine("Finished");
    Console.ReadLine();
}

Nowy wątek to obiekt typu Thread. W konstruktorze tego obiektu  podajemy nowy obiekt ParametrizedThreadStart do którego konstruktora podajemy naszą metodę Add.
Dzięki temu że jest to obiekt ParametrizedThreadStart możliwe jest w następnej linii podanie obiektu z parametrami: t.Start(ap).

Tutaj uwidoczni się teraz problem synchronizacji. Gdyby nie Console.ReadLine w głownej metodzie Main() to program zamknął by się natychmiast. Tutaj pokazane przykładowe działanie:

Jak widać wynik metody Add został obliczony dopiero po tym jak główny wątek zakończyłby już program. Zasymulowaliśmy to za pomocą Thread.Sleep(100);

Synchronizacja wątków

Aby wątek główny czekał na wątek poboczny do chwili jego zakończenia zdefiniujmy statyczny obiekt AutoResetEvent przed metodą Main():

private static AutoResetEvent waitHandle = new AutoResetEvent(false);

Przy konstruktorze podajemy wartość false. Oznacza to że nie przesłano jeszcze powiadomienia o zakończenia wątku pobocznego.
Teraz jeśli chcemy aby wątek główny rozpoczął oczekiwanie na wątek poboczny dopiszmy:

waitHandle.WaitOne();

Teraz w funkcji Add(..) aby poinformować o zakończeniu pracy wystarczy dopisać:

waitHandle.Set();

A efekt działania programu po tej zmianie:

Jak widać wątek główny zakończył się natychmiast po zakończeniu wątku pobocznego.
A teraz cały kod dla jasności:
Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Threads1
{
    class Program
    {
        private static AutoResetEvent waitHandle = new AutoResetEvent(false);

        static void Main(string[] args)
        {
            AddParams ap = new AddParams(10, 10);
            Thread t = new Thread(new ParameterizedThreadStart(Add));
            t.Start(ap);
            waitHandle.WaitOne();
            Console.WriteLine("Finished");
            Console.ReadLine();
        }
        static void Add(object data)
        {
            Console.WriteLine("ID wątku metody Add(): {0}",
                Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(100);
            AddParams ap = (AddParams)data;
            Console.WriteLine("{0} + {1} = {2}",
                ap.a, ap.b, ap.a + ap.b);
            waitHandle.Set();
        }
    }
}


AddParams.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Threads1
{
    class AddParams
    {
        public int a, b;

        public AddParams(int num1, int num2)
        {
            a = num1;
            b = num2;
        }
    }
}

środa, 23 maja 2012

Szybkie dodawanie przestrzeni nazw

Jeśli znamy obiekt z którego chcemy skorzystać a nie pamiętamy w jakiej przestrzeni nazw się znajduje (nie wiemy co dopisać po "using") z pomocą przychodzi VisualStudio.

Załóżmy że chcemy użyć wielowątkowości w naszym programie. Potrzebujemy dostać się do klasy Thread. Jednak w jakiej przestrzeni nazw ona się znajduje?


Aby automatycznie dodać odpowiednią przestrzeń nazw wystarczy kliknąć na "Thread" prawym przyciskiem myszy i wybrać Resolve->using System.Threading;

Odpowiednia przestrzeń nazw zostanie dodana automatycznie (jednak musimy mieć w projekcie odpowiednią referencję do biblioteki).

Asynchroniczne wywołanie delegatów

Pora na wielowątkowość!
W najbliższych postach zajmę się wielowątkowością w C#. Jednak zanim przejdę do "prawdziwych" watków (z przestrzeni System.Threading), najpierw asynchroniczne delegaty.

Asynchroniczne delegaty

Delegata można utożsamiać ze wskaźnikiem na funkcję z zachowaniem bezpieczeństwa typów. Takiego delegata można wywołać synchronicznie i asynchronicznie. Asynchroniczne wywołanie delegata daje takie same rezultaty jak utworzenie nowego wątku a jest nawet prostrze! 
Jak się okaże, wystarczy przypisać delegatowi pewną funkcję i zamiast wywołać delegata jak zwykła funkcję użyta zostanie metoda BeginInvoke().

Rzućmy okiem na poniższy programik:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace AsyncDelegate
{
    public delegate int BinaryDelegate(int x, int y);
    class Program
    {
        private static bool isDone = false;

        static void Main(string[] args)
        {
            Console.WriteLine("**** Asynchroniczne delegaty ****");
            Console.WriteLine("Main() wywołana na wątku {0}.",
                Thread.CurrentThread.ManagedThreadId);

            BinaryDelegate b = new BinaryDelegate(Add);
            IAsyncResult theAsRes = b.BeginInvoke(10, 10,
                new AsyncCallback(AddComplete), "Dzięki za współpracę!");

            while (!isDone)
            {
                Thread.Sleep(1000);
                Console.WriteLine("WORKING...");
            }
            //Console.WriteLine("WAITING");
            Console.ReadLine();
        }

        static int Add(int x, int y)
        {
            Console.WriteLine("Add() wywołana na wątku {0}.",
                Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(5000);
            return x + y;
        }

        static void AddComplete(IAsyncResult theAsRes)
        {
            Console.WriteLine("AddComplete() wywołane na wątku {0}.",
                Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("Dodawanie skończone");
            Console.WriteLine((string)theAsRes.AsyncState);
            isDone = true;
        }
    }
}

Teraz po kolei wszystko sobie wyjaśnimy.

  • Jak widać delegat nie musi znajdować się wewnątrz klasy, może być poza nią.
  • Do podejrzenia identyfikatora wątku używamy Thread.CurrentThread.ManagedThreadId
  • Metoda BeginInvoke zawsze zwraca obiekt implementujący interfejs IAsyncResult. Ten obiekt to pewnego rodzaju sprzężenie pozwalające wątkowi wywołującemu (czyli temu głównemu) uzyskać wynik asynchronicznego wywołania metody w późniejszym czasie (np przy pomocy EndInvoke(), którą tutaj pominąłem).
  • Do synchronizacji użyłem tutaj zmienną typu bool. Nie jest to dobra praktyka lecz dla tego przykładu w zupełności wystarczająca. (Nie jest dobra dlatego że każdy z wątków ma do niej jednoczesny dostęp... ale o tym później)
  • Pętla co sekundę sprawdza czy dodawanie już się zakończyło. Zapobiega ona zakończeniu programu zanim wątek poboczny obliczy wynik.

Jednak pozostało jeszcze do rozstrzygnięcia coś dziwnego.
Normalnie wywołując delegata (czy to tak jak funkcję czy za pomocą invoke) podawalibyśmy 2 parametry (agrumenty funkcji "połaczonych" z delegatem). Jednak dla BeginInvoke potrzeba 4 parametrów.

  • Za pomocą pierwszego parametru podaje się funkcję, która zostanie wywołana po zakończeniu wątku pobocznego.
  • Drugi parametr jest typu Object. Pozwala przesłać dowolny obiekt do wybranej wcześniej funkcji.
I tak:
Tworząc nowego delegata AssyncCallback i podając mu jako parametr nazwę funkcji AddComplete (która przyjmuje obiekt IAsyncResult) mamy pewność że funkcja zostanie wywołana natychmiast po zakończeniu wątku pobocznego. Dodatkowo dzięki czwartemu parametrowi, przy pomocy IAsyncResult theAsRes możemy "wyciągnąć" dodatkowy argument przesłany do funkcji przez rzutowanie jego właściwości AsyncState na string.

czwartek, 17 maja 2012

MouseLeftButtonDown na DataGrid

Ostatnio spotkałem się z pewnym problemem związanym z DataGridem. Potrzebowałem oprogramować na nim zdarzenie MouseLeftButtonDown. Jednak nie jest to takie proste. Do pokazania problemu i rozwiązania użyję prostego projektu Silverlight Application.

Tworzenie projektu Silverlight

Utwórzmy zwykły najprostrzy projekt Silverlight Application w Visual Studio.
Za pomocą toolboxa lub bezpośrednio w XAML dodajmy do niego obiekt DataGrid.
Utwórzmy dodatkowo nową klasę Person, której obiektami wypełnimy automatycznie DataGrida. Tutaj przykład klasy:

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

Następnie zaraz po InitializeComponent() w metodzie MainPage() zapiszmy 100 losowych osób do naszego DataGrida.
        List<person> persons = new List<person>();
        for(int i=0;i<100;++i){
            persons.Add(new Person{ FirstName = "Name " + i, LastName = "LastName " + i});
        }
        dataGrid1.AutoGenerateColumns = true;
        dataGrid1.ItemsSource = persons;

Po uruchomieniu projektu powinniśmy zobaczyć wynik podobny do tego:

Problem

Aby przekonać się o problemie MouseLeftButtonDown spróbujmy dopisać reagowanie na to właśnie zdarzenie poprzez wyświetlenie MessageBoxa. Tutaj przykładowa metoda:
private void dataGrid1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    MessageBox.Show("Mouse Pressed!");
}

Zauważymy że tylko po kliknięciu na górną belkę z nazwami kolumn lub na jedno-pixelową obwódkę wokół naszej kontrolki następuje wyświetlenie MessageBoxa:

A co jeśli np tak jak ja, potrzebujemy koniecznie wyzwolić to zdarzenie po kliknięciu na dany rekord wewnątrz kontrolki? Jak zrobić żeby nasz event nie został "zjedzony" przez wiersze tabeli?

Rozwiązanie problemu

Utworzymy własną kontrolkę dziedziczącą po  DataGrid.
public class MyDataGrid : DataGrid
{
    public MyDataGrid()
    {
        this.AddHandler(FrameworkElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(MouseDown), true);
    }
    void MouseDown(object sender, MouseButtonEventArgs e)
    {
        MessageBox.Show("Mouse Left Button Down!");
    }
}

Konstruktor naszego nowego obiektu wywołuje metodę AddHandler, która doda odpowiedni Event do obsłużenia. Dodatkowo jako parametr musimy podać nowy obiekt MouseButtonEventHandler który inicjujemy podając mu nazwę metody wywoływanej przy wciśnięciu lewego przycisku myszki.

Z poprzedniego pliku MainPage.xaml.cs usuńmy wcześniej dopisaną metodę dataGrid1_MouseLeftButtonDown() pamiętając o usunięciu jej także z pliku xaml.
Dodatkowo w pliku xaml zdefiniujmy naszą przestrzeń nazw, w tym przypadku:
xmlns:my="clr-namespace:SilverlightApplication1"

Możemy teraz zmienić znacznik <sdk:datagrid> na <my:mydatagrid> pamiętając o zmianie parametru Name na x:Name.
Jeśli wszystko wykonaliśmy dobrze to po kliknięciu na dane w naszym nowym DataGridzie uzyskamy obsługę zdarzenia kliknięcia lewego przycisku myszki:
Poniżej całkowity kod MainPage.xaml.cs:
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace SilverlightApplication1
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            List persons = new List();
            for(int i=0;i<100;++i){
                persons.Add(new Person{ FirstName = "Name " + i, LastName = "LastName " + i});
            }
            dataGrid1.AutoGenerateColumns = true;
            dataGrid1.ItemsSource = persons;
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}


MainPage.xaml:


    
        
    



MyDataGrid.cs:
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace SilverlightApplication1
{
    public class MyDataGrid : DataGrid
    {
        public MyDataGrid()
        {
            this.AddHandler(FrameworkElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(MouseDown), true);
        }
        void MouseDown(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("Mouse Left Button Down!");
        }
    }
}



poniedziałek, 7 maja 2012

Otwieranie pliku w trybie binarnym Visual Studio

Czasami jeśli nie znamy struktury jakiegoś pliku a chcemy wyciągnąć z niego pewne informacje, przydaje się podejrzenie jego zawartości binarnej. Są do tego celu stworzone specjalne edytory chociażby WinHex.
Jednak jeśli nie zależy nam na głębszej analizie i różnego typu pomocniczych opcjach a chcemy jedynie szybko podejrzeć zawartość pliku to Visual Studio wystarcza zupełnie.

Z otwieraniem plików w trybie binarnym w VS wiąże się pewien "myk".

Robimy kolejno File-> Open-> File:



Wybieramy plik i zamiast Enter czy kliknięcia przycisku Open klikamy strzałkę na tym przycisku:


Następnie Open With...


I wybieramy Binary Editor


Poniżej widzimy otwarty w trybie binarnym plik .tif który zawiera w sobie wielowymiarowe obrazy komórki (obrazy dwuwymiarowe robione dla kolejnych głębokości i w kolejnych odstępach czasu (4D) ). Widać tutaj ładnie takie parametry jak ilość przekrojów (głębokości) - slices oraz ilość klatek na jedną głębokość (16) a także ilość wszystkich obrazków (images):


Zapis danych do pliku binarnego

Dzisiaj dowiemy się jak zapisać i odczytać dane z pliku binarnego - prosta przydatna rzecz

Binary writer

W nowym konsolowym projekcie napiszmy następującą metodę main:

static void Main(string[] args)
    {
        Console.WriteLine("Test zapisu binarnego");
        FileInfo f = new FileInfo("Binfile.bin");
        using (BinaryWriter bw = new BinaryWriter(f.OpenWrite()))
        {
            double zm1 = 12345.678;
            int zm2 = 32123;
            string zm3 = "Test-string";

            bw.Write(zm1);
            bw.Write(zm2);
            bw.Write(zm3);
        }
        Console.ReadLine();
    }

Ponieważ używamy tutaj obiektu FileInfo należy pamiętać o dopisaniu using System.IO;


Kod nie wymaga chyba większego komentarza. Tworzone są 3 zmienne a następnie zapisywane do pliku Binfile.bin.


Binary reader

Jeśli otworzylibyśmy teraz plik Binfile.dat w notatniku to (poza zmienną string) nie wiele byśmy zobaczyli:


Nie tylko notatnik a nawet bezpośredni podgląd pliku binarnego też nie wiele pokazuje:



Spróbujmy zatem odczytać z powrotem te dane za pomocą binary readera.
Do naszej metody main powyżej Console.ReadLine(); dopiszmy:

using (BinaryReader br = new BinaryReader(f.OpenRead()))
        {
            Console.WriteLine(br.ReadDouble());
            Console.WriteLine(br.ReadInt32());
            Console.WriteLine(br.ReadString());
        }

A wynik:


Jak widać wszystko zgodnie z planem.

niedziela, 6 maja 2012

WPF Thumb

Ostatnio pisałem swoją własną wtyczkę efektową VST przy użyciu C# WPF i wrappera VstNET.
Do ukończenia mojego efektu musiałem rozwiązać pewien problem: przesuwanie i zmiana rozmiaru własnej kontrolki WPF.

Próbowałem najpierw zrobić to "po swojemu" czyli reagować odpowiednio na zdarzenia kliknięcia lewego przycisku myszki i ruchu myszki. Jednak efekt nie był zadowalający. Gdy ruchy myszką były za szybkie, kursor wylatywał poza obszar reagujący na kliknięcie i kontrolka przestawała się przesuwać.

Na szczęście WPF posiada lepsze rozwiązania a mianowicie Thumb.

Jest to obiekt który reaguje na zdarzenie bardzo pomocne przy przesuwaniu i zmianie rozmiarów własnych obiektów - DragDelta.

W moim przypadku wyglądało to tak:

Na screenie widać 4 kontrolki (jedna jest oznaczona strzałkami) które reprezentują pasma przepustowe dla filtra opartego na FFT. Chciałem aby poprzez zmianę rozmiarów i przesuwanie ich, wpływały na przetwarzanie sygnału. Thumb okazał sie idealny do tego rozwiązania.

Strzałki na rysunku pokazują 3 obiekty Thumb. Te po bokach odpowiadają za zmianę rozmiaru a całe środkowe wypełnienie za przesunięcie w poziomie.

Poniżej fragment kodu XAML kontrolki "BandControl"



    
        
              
        
    
    
        
            
                
                
                
            
        
    
    
    
    
    


Widać że dodatkowo został zdefiniowany szablon dla Thumba. Jest to niezbędne jeśli chce się uzyskać inny wygląd niż standardowy "trójwymiarowy" szary prostokąt (coś jak brzegi okien w starszych windowsach).
Wewnątrz szablonu, kolorowy prostokąt przyjmuje tło takie jak wpisane zostanie do kontrolki Thumb za pomocą TemplateBinding.
Dalej w XAMLu wystarszy dla każdego Thumba przypisać odpowiedni szablon i gotowe.

W konstruktorze kontrolki w pliku .cs dla każdego Thumba przypisałem odpowiednią metodę do zdarzenia DragDelta:
public BandControl()
{
 this.InitializeComponent();
 moveArea.DragDelta += MoveThumb_DragDelta;
 resizeLeft.DragDelta += ResizeLeft_DragDelta;
 resizeRight.DragDelta += ResizeRight_DragDelta;
}


Powyższe metody zaimplementowałem w następujący sposób:
private void MoveThumb_DragDelta(object sender, DragDeltaEventArgs e)
        {
   
   if(this.Margin.Left + e.HorizontalChange > 0 && (this.Margin.Left + this.Width + e.HorizontalChange) < parentWidth)
   this.Margin = new Thickness(
    this.Margin.Left + e.HorizontalChange,
    this.Margin.Top,
    this.Margin.Right,
    this.Margin.Bottom);
   RaiseBarChangedEvent();
        }
  
  private void ResizeLeft_DragDelta(object sender, DragDeltaEventArgs e)
        {
            if((this.ActualWidth - e.HorizontalChange) > 10 && (this.Margin.Left + e.HorizontalChange) > 0){
    this.Margin = new Thickness(
     this.Margin.Left + e.HorizontalChange,
     this.Margin.Top,
     this.Margin.Right,
     this.Margin.Bottom);
    this.Width -= e.HorizontalChange;
    e.Handled = true;
   }
   RaiseBarChangedEvent();
        }
  
  private void ResizeRight_DragDelta(object sender, DragDeltaEventArgs e)
  {
   if((this.ActualWidth + e.HorizontalChange) > 10 && (this.Margin.Left + this.Width + e.HorizontalChange) < parentWidth){
    this.Width += e.HorizontalChange;
    e.Handled = true;
   }
   RaiseBarChangedEvent();
  }

Dzięki temu mamy pewność że kontrolki nie wydostaną się poza obszar rodzica.
Rozmiar rodzica jest znany dzięki dodaniu nowego konstruktora przyjmującego jako parametr kontrolkę o poziom wyżej:
  public BandControl(Display d) : this() {
   parentWidth = d.Width;
  }

Polecam także ten link, z którego korzystałem aby dowiedzieć się jak działa Thumb.