Pokazywanie postów oznaczonych etykietą WF. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą WF. Pokaż wszystkie posty

wtorek, 17 maja 2011

Virtual Mode - Windows Forms DataGridView

Pisząc ostatnio aplikację w WindowsForms musiałem wyświetlić wyniki w Gridzie. Oczywiście pisząc programy operujące na danych bardzo często używamy komponentu DataGridView do wyświetlania danych tabelarycznych.
Wyświetlanie oczywiście nie jest trudne: tworzymy źródło danych, podpinamy pod Grida i już możemy zobaczyć efekty na ekranie.
Rozwiązanie szybkie, proste i właściwie przez pewien czas będzie bardzo dobrze działało. Jak wiadomo co przychodzi łatwo i przyjemnie szybko się kończy. Tworząc aplikację najczęściej testujemy ją na kilku rekordach. Aplikacja działa wtedy bardzo szybko i nie widać żadnego działania niepożądanego. 5 tys rekordów? Dalej wszystko działa sprawnie i bez przeszkód. Przenosimy się w przyszłość - 50 tys rekordów. Ładowanie danych zaczyna zajmować trochę więcej czasu niż normalnie - niekiedy jest to 10 sek w zależności od ilości kolumn.
Teraz skaczemy do odległej przyszłości która może wyglądać następująco:


Jeżeli ktoś nie dowierza - to tak, mamy do czynienia z tabelą przechowującą ponad 6 mln rekordów.

Przejdźmy na chwilę do kodu. Na początek zastanówmy się w co można by było załadować taką ilość danych. Wydawało by się, że DataTable będzie najlepszym i najszybszym wyborem - od razu zaznaczam nie jest. Zużycie pamięci podczas ładowania danych sięgnęło gigantycznych wartości rzędu 1,6 GB - zaznaczam że te dane jeszcze nie trafiły do Grida. Dane ładowały się bardzo długo - właściwie nie jest to dopuszczalne zachowanie aplikacji.
Tak więc następujący kod nie spełni swojego zadania:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Diagnostics;

namespace WindowsFormsApplication3
{
    public class TestTable
    {
        private string ConnectionString;

        public TestTable()
        {
            ConnectionString = ConfigurationManager.ConnectionStrings["db"].ConnectionString;
        }

        public DataTable GetData()
        {
            Stopwatch time = new Stopwatch();
            time.Start();
            DataTable data = new DataTable();
            using (SqlConnection conn = new SqlConnection(ConnectionString))
            {
                string sql = @"SELECT [Id],[FirstName],[LastName],[BirthYear],[ChildernAmount] FROM [test4].[dbo].[test]";
                using (SqlCommand cmd = new SqlCommand(sql, conn))
                {
                    try
                    {
                        conn.Open();
                        using (SqlDataReader dr = cmd.ExecuteReader())
                        {
                            data.Load(dr);
                            dr.Close();
                        }
                    }
                    catch
                    {

                    }
                    finally
                    {
                        conn.Close();
                    }
                }
            }
            time.Stop();
            Console.WriteLine(time.Elapsed.Seconds + " " + time.Elapsed.Milliseconds);

            return data;
        }
    }
}

Czas zastanowić się w co można innego "włożyć nasze dane". Spróbujmy z listą:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Diagnostics;

namespace WindowsFormsApplication3
{
    public class TestTable
    {
        private string ConnectionString;

        public TestTable()
        {
            ConnectionString = ConfigurationManager.ConnectionStrings["db"].ConnectionString;
        }

        public List<Test> GetData()
        {
            Stopwatch time = new Stopwatch();
            time.Start();
            List<Test> data = null;
            using (SqlConnection conn = new SqlConnection(ConnectionString))
            {
                string sql = @"SELECT Count(1) FROM [test4].[dbo].[test]";
                using (SqlCommand cmd = new SqlCommand(sql, conn))
                {
                    try
                    {
                        conn.Open();
                        int recordsCount = int.Parse(cmd.ExecuteScalar().ToString());
                        data = new List<Test>(recordsCount);
                        sql = @"SELECT [Id],[FirstName],[LastName],[BirthYear],[ChildernAmount] FROM [test4].[dbo].[test]";
                        cmd.CommandText = sql;
                        using (SqlDataReader dr = cmd.ExecuteReader())
                        {
                            while (dr.Read())
                            {
                                Test t = new Test();
                                t.Id = int.Parse(dr["Id"].ToString());
                                t.FirstName = dr["FirstName"].ToString();
                                t.LastName = dr["LastName"].ToString();
                                t.BirthYear = int.Parse(dr["BirthYear"].ToString());
                                t.ChildernAmount = int.Parse(dr["ChildernAmount"].ToString());
                                data.Add(t);
                            }
                            dr.Close();
                        }
                    }
                    catch
                    {

                    }
                    finally
                    {
                        conn.Close();
                    }
                }
            }
            time.Stop();
            Console.WriteLine(time.Elapsed.Seconds + " " + time.Elapsed.Milliseconds);

            return data;
        }
    }
}

Pierwsza sprawa to duży spadek zużycia pamięci - użycie listy zajęło 564 MB. Czas ładowania jest jednak nadal duży i wynosi ponad 40 sek. Zbindowanie danych z DataGridView kosztuje już tylko 110 MB.
Oczywiście przechodzenie po gridzie jest bardzo powolne i nie wydajne. Sprawia wrażenie chodzenia po polu minowym - naciskając strzałkę w dół czekamy 1 sek na reakcję aby selektor wiersza przeszedł na następny rekord.

Jak więc poradzić sobie z dużą ilością danych w naszej aplikacji? Istnieje kilka sposobów na to zadanie.
Jednym z rozwiązań jest zastosowanie VirtualMode - czyli trybu wirtualnego. Czym zajmuje się ten tryb. Otóż jego zadanie jest bardzo proste - pobiera i wyświetla tylko te rekordy które aktualnie są potrzebne do tego aby użytkownik mógł widzieć ich zawartość. Łatwo to zobrazować na diagramie. W przypadku zwykłego bindowania mamy do czynienia z następującą sytuacją:


W przypadku trybu wirtualnego pobieramy tylko część danych z bazy:


Wiemy już w jaki sposób działa tryb wirtualny, teraz przejdźmy do tego co jest potrzebne aby go zaimplementować do
1. Ustawiamy atrybut VirtualMode kontrolki DataGridView na true
2. Kodujemy zdarzene trybu wirtualnego: CellValueNeeded
3. Ustawiamy właściwość RowCount kontrolki DataGridView

Ilość rekordów potrzebna jest, aby DataGridView wiedział ile pustych rekordów ma wstawić do kontrolki.
Przejdźmy więc do sedna rozwiązania:

Na początek przygotujemy klasę, która posłuży jako bufor wczytywania potrzebnych danych:


Code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Diagnostics;

namespace WindowsFormsApplication3
{
    public class TestTable
    {
        private string ConnectionString;
        public Test[] Items { get; set; }

        public int PageSize { get; set; }
        public int TotalRecordCount { get; set; }
        private int currentPage;
        private int loadedPages;
        private int lastPage;

        public TestTable(int pageSize = 500)
        {
            ConnectionString = ConfigurationManager.ConnectionStrings["db"].ConnectionString;
            TotalRecordCount = RowCount();
            lastPage = TotalRecordCount / pageSize;
            loadedPages = -1;
            currentPage = -1;
            PageSize = pageSize;
            Items = new Test[TotalRecordCount];
        }

        public List<Test> GetData()
        {
            Stopwatch time = new Stopwatch();
            time.Start();
            List<Test> data = null;
            using (SqlConnection conn = new SqlConnection(ConnectionString))
            {
                string sql = @"SELECT Count(1) FROM [test4].[dbo].[test]";
                using (SqlCommand cmd = new SqlCommand(sql, conn))
                {
                    try
                    {
                        conn.Open();
                        int recordsCount = int.Parse(cmd.ExecuteScalar().ToString());
                        data = new List<Test>(recordsCount);
                        sql = @"SELECT [Id],[FirstName],[LastName],[BirthYear],[ChildernAmount] FROM [test4].[dbo].[test]";
                        cmd.CommandText = sql;
                        using (SqlDataReader dr = cmd.ExecuteReader())
                        {
                            while (dr.Read())
                            {
                                Test t = new Test();
                                t.Id = int.Parse(dr["Id"].ToString());
                                t.FirstName = dr["FirstName"].ToString();
                                t.LastName = dr["LastName"].ToString();
                                t.BirthYear = int.Parse(dr["BirthYear"].ToString());
                                t.ChildernAmount = int.Parse(dr["ChildernAmount"].ToString());
                                data.Add(t);
                            }
                            dr.Close();
                        }
                    }
                    catch
                    {

                    }
                    finally
                    {
                        conn.Close();
                    }
                }
            }
            time.Stop();
            Console.WriteLine(time.Elapsed.Seconds + " " + time.Elapsed.Milliseconds);

            return data;
        }

        public int RowCount()
        {
            int count = 0;
            using (SqlConnection conn = new SqlConnection(ConnectionString))
            {
                string sql = @"SELECT Count(1) FROM [test4].[dbo].[test]";
                using (SqlCommand cmd = new SqlCommand(sql, conn))
                {
                    try
                    {
                        conn.Open();
                        count = int.Parse(cmd.ExecuteScalar().ToString());
                    }
                    catch
                    {

                    }
                    finally
                    {
                        conn.Close();
                    }
                }
            }
            return count;
        }

        public void LoadData(int rowNumber)
        {
            ++rowNumber;
            currentPage = rowNumber / PageSize;
            if (currentPage > loadedPages && currentPage <= lastPage)
            {
                loadedPages = currentPage;
                GetNextPage();
            }
        }

        public void GetNextPage()
        {
            int startIndex = currentPage * PageSize;
            int endIndex = startIndex + PageSize;
            using (SqlConnection conn = new SqlConnection(ConnectionString))
            {
                string sql = @"SELECT [Id],[FirstName],[LastName],[BirthYear],
                                    [ChildernAmount] FROM [test4].[dbo].[test]
                                WHERE Id BETWEEN @startIndex AND @endIndex";
                using (SqlCommand cmd = new SqlCommand(sql, conn))
                {
                    try
                    {
                        cmd.Parameters.AddWithValue("@startIndex", startIndex);
                        cmd.Parameters.AddWithValue("@endIndex", endIndex);
                        conn.Open();
                        using (SqlDataReader dr = cmd.ExecuteReader())
                        {
                            while (dr.Read())
                            {
                                Test t = new Test();
                                t.Id = int.Parse(dr["Id"].ToString());
                                t.FirstName = dr["FirstName"].ToString();
                                t.LastName = dr["LastName"].ToString();
                                t.BirthYear = int.Parse(dr["BirthYear"].ToString());
                                t.ChildernAmount = int.Parse(dr["ChildernAmount"].ToString());
                                this.Items[startIndex] = t;
                                startIndex++;
                            }
                            dr.Close();
                        }
                    }
                    catch
                    {

                    }
                    finally
                    {
                        conn.Close();
                    }
                }
            }
        }
    }
}

Zasada działania jest prosta. Ustalamy na początku ile rekordów jest w bazie, wielkość bufora wczytywania (domyślnie 500 elementów). Następnie wyliczamy ile będzie wszystkich stron. Jeżeli indeks wiersza który chce otrzymać użytkownik przekracza aktualnie wczytanych, wtedy wczytujemy następną porcję danych. Na formie umieszczamy następujący kod:


Code:
TestTable table = new TestTable();

        public Form1()
        {
            InitializeComponent();
            dataGridView1.RowCount = table.RowCount();
            dataGridView1.ColumnCount = 5;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            List<Test> tab = table.GetData();
            dataGridView1.DataSource = tab;
        }

        private void dataGridView1_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
        {
            table.LoadData(e.RowIndex);
            switch (e.ColumnIndex)
            {
                case 0:
                    e.Value = table.Items[e.RowIndex].Id;
                    break;
                case 1:
                    e.Value = table.Items[e.RowIndex].FirstName;
                    break;
                case 2:
                    e.Value = table.Items[e.RowIndex].LastName;
                    break;
                case 3:
                    e.Value = table.Items[e.RowIndex].BirthYear;
                    break;
                case 4:
                    e.Value = table.Items[e.RowIndex].ChildernAmount;
                    break;
            }
        }

Tutaj obsługujemy zdarzenie CellValueNeeded, czyli podajemy dane dla komórki która potrzebuje aktualnie dane (pamiętać należy, że zdarzenie to wywołuje się dla każdej komórki z osobna). Oczywiście aby wszystko działało należy ustawić parametry ilości kolumn w DataGridView oraz ilość całkowitą wierszy (na tej podstawie DataGridView wygeneruje odpowiednią ilość pustych wierszy które następnie będzie zapełniać).

Efektem pracy będzie grid pozwalający na załadowanie 6 mln rekordów bez bardziej widocznego spowolnienia:


Oczywiście należy pamiętać, że nie jest to jedyny a na pewno nie najlepszy sposób rozwiązania problemu dużej ilości informacji. 6 mld rekordów w tabeli nie jest powalającą liczbą. Istnieją systemy posiadające w swoich tabelach miliardy rekordów. Załadowanie takiej ilości danych może jedynie skonsumować ogromną ilość GB ramu.
Aby zapobiec temu zjawisku należy zastosować inną metodę pobierania danych do wyświetlania. Inne metody które można zaimplementować do wyświetlania danych w gridzie to:
- pobieranie i wyświetlanie tylko części danych i przeładowywanie źródła ciągle.
- filtrowanie danych - wyświetlanie tylko części danych np. z ostatniego tygodnia

Danych z dnia na dzień przybywa, odpowiednia ich obróbka oraz przechowywanie to z pewnością jedno z największych wyzwań obecnych czasów.

niedziela, 4 lipca 2010

Bindowanie danych w WindowsForms

Bindowanie danych, czyli na polski wiązanie danych z kontrolką to proces w którym pobieramy dane i łączymy je z kontrolką na formie.

Wiązanie takie może być proste, złożone oraz w jedną stronę jak i w dwie strony.

Przyjrzyjmy się najpierw prostemu wiązaniu z kontrolką typu TextBox. Proste wiązanie polega na zbindowaniu źródła danych z jedną lub wieloma właściwościami kontrolki. Napiszemy klasę z którą następnie zbindujemy TextBox:

    public class TextBoxBinding
    {
        private string _text;

        public string Text
        {
            get { return _text; }
            set { _text = value; }
        }

        private Color _textColor;

        public Color TextColor
        {
            get { return _textColor; }
            set { _textColor = value; }
        }

        private Color _backColor;

        public Color BackColor
        {
            get { return _backColor; }
            set { _backColor = value; }
        }
    }

Sam kod bindowania wygląda natomiast tak:

            TextBoxBinding binding = new TextBoxBinding()
            {
                BackColor = Color.Blue,
                TextColor = Color.Red,
                Text = "Simple Binding"
            };

            textBox1.DataBindings.Add("Text", binding, "Text");
            textBox1.DataBindings.Add("ForeColor", binding, "TextColor");
            textBox1.DataBindings.Add("BackColor", binding, "BackColor");

DataBindings jest typu kolekcja. Co za tym idzie dodajemy kolejne elementy typu Binding lub tak jak pokazano wprowadzamy od razu wartości w kolejności:
textBox1.DataBindings.Add("Właściwość kontrolki, którą bindujemy ze źródłem", obiekt zawierający dane bindowania, "właściwość obiektu z którym bindujemy właściwość kontrolki");
Ostatni parametr możemy pominąć (null), jeżeli klasa z którą bindujemy kontrolkę zawiera przeładowaną metodę ToString().

Daną właściwość kontrolki można bindować tylko raz. W przypadku wywołania poniższego kodu otrzymamy error:

            textBox1.DataBindings.Add("Text", binding, "Text");
            binding.Text = "Ala ma kota";
            textBox1.DataBindings.Add("Text", binding, "Text");

Aby zbindować ponownie właściwość Text kontrolki TextBox, należy usunąć wpierw stare bindowanie a następnie przypisać nowe:

            textBox1.DataBindings.Add("Text", binding, "Text");
            binding.Text = "Ala ma kota";
            if (textBox1.DataBindings["Text"] != null)
            {
                textBox1.DataBindings.Remove(textBox1.DataBindings["Text"]);
            }
            textBox1.DataBindings.Add("Text", binding, "Text");

W przypadku bindowania do kolekcji obiektów należy wskazać do którego z obiektów ma zostać zbindowana dana właściwość:

            TextBoxBinding binding = new TextBoxBinding()
            {
                BackColor = Color.Blue,
                TextColor = Color.Red,
                Text = "Simple Binding"
            };
            List<TextBoxBinding> arrBinding = new List<TextBoxBinding>
            {
                binding,
                new TextBoxBinding { BackColor = Color.PowderBlue, Text = "Ala ma kota", TextColor = Color.Black},
                new TextBoxBinding { BackColor = Color.Brown, TextColor = Color.White, Text = "A kot ma Ale"}
            };

            textBox1.DataBindings.Add("Text", arrBinding[1], "Text");

Jeżeli tego nie zrobimy, zostanie wybrany domyślnie element o indeksie 0 z kolekcji.

Przejdźmy teraz do złożonego bindowania. Stworzymy na potrzeby tego przykładu nową klasę:

    class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int YearOfBirth { get; set; }
        public Person(string firstName, string lastName, int yearOfBirth)
        {
            FirstName = firstName;
            LastName = lastName;
            YearOfBirth = yearOfBirth;
        }
    }

Na formę kładziemy dwa TextBoxy oraz ListBox oraz przycisk. W kodzie dla eventu Click w przycisku wprowadzamy kod:

            List<Person> people = new List<Person>
            {
                new Person("Jan", "Kowlaski", 1998),
                new Person("Sebastian", "Potaciński", 2001),
                new Person("Mariusz", "Rozberg", 2001),
                new Person("Piotrek", "Simple", 1998),
                new Person("Łukasz", "Niewadzki", 1999),
                new Person("Maria", "Prowadzki", 1965),
            };

            listBox1.DataSource = people;
            listBox1.DisplayMember = "FirstName";
            textBox1.DataBindings.Add("Text", people, "LastName");
            textBox2.DataBindings.Add("Text", people, "YearOfBirth");

Uruchamiamy aplikację. Dzięki takiemu bindowaniu, w ListBoxie widzimy imiona osób a szczegóły o nich podawane są w TextBoxach. Taki widok osiągamy dzięki Binding Manager, który zarządza bindowanymi elementami. Należy pamiętać, że zaawansowane wiązanie działa tylko z kontrolkami które mogą wyświtelać na raz wiele elementów (ListBox, ListView, ComboBox itd.).

Teraz czas na bindowanie w jedną i wiele stron. Jak można się domyślić bindowanie w jedną stronę to po prostu umożliwienie przeglądania danych z jakiegoś źródła. W przypadku bindowania w obie strony, uaktualniając wyświetlaną wartość w kontrolce uaktualniamy także dane w źródle danych:


W przypadku zwykłych obiektów sprawa jest bardzo prosta. Aby umożliwić dwustronną wymianę danych, należy umożliwić zapis danych do danego pola, czyli w naszym przypadku udostępnić metody set i get:

        public int YearOfBirth { get; set; }

Jeżeli teraz spróbujemy zmienić imię którejś z osób, zmiana zostanie od razu zapisana. Jeżeli daną właściwość oznaczymy jako prywatną:

        public string LastName { get; private set; }

po wprowadzeniu nowej wartości do kontrolki nie zostanie ona zapisana do źródła. Ważne jest uświadomienie sobie, że zapis następuje dopiero w momencie kiedy opuszczamy kontrolkę (przechodzimy do kolejnej).

Kiedy bindujemy kontrolkę do danych z bazy danych, należy samemu zatroszczyć się o uaktualnianie danych w tym źródle. Można to osiągnąć np. poprzez użycie DataAdaptera i wywołanie metody Update dla danego DataSetu i tabeli.

Kontrolką która nadaje się świetnie do bindowania danych tabelarycznych jest DataGridView. Pozwala na szeroki wachlarz zastosowań wyświetlania jak i manipulowania danymi.
Oprócz wymienionych cech DataGridView może bindować bardzo duże ilości danych w krótkim czasie. Dodanie nowego wiersza do źródła danych uaktualnia samoczynnie kontrolkę. Dzięki trybowi VirtualMode pozwala na łatwe zarządzanie bardzo dużymi ilościami wierszy.

środa, 25 listopada 2009

Drukowanie

Drukowanie jest naturalną czynnością. W przypadku C# dysponujemy wieloma gotowymi metodami i klasami ułatwiającymi to zadanie. Artykuł rozpocznę od najbardziej elementarnych czynności które należy podjąć w celu wydrukowania strony, aż po bardziej zaawansowane mechanizmy. 



 

Każda operacja drukowania rozpoczyna się od utworzenia obiektu klasy PrintDocument. Cały proces drukowania rozpoczyna się w momencie wywołania metody Print klasy PrintDocument. Wywołanie tej metody pociąga za sobą wywołanie zdarzeń: BeginPrint, PrintPage, EndPrint. Jak z samych nazw można się domyślić kolejne zdarzenia oznaczają: rozpoczęcie procesu drukowania, drukowanie strony, zakończenie drukowania. Szczególnie ciekawe jest zdarzenie PrintPage. Jest ono wywoływane podczas drukowania każdej nowej strony. Zdarzenie to, na podstawie właściwości HasMorePages przesyła do drukarki informację, czy do drukowania pozostały jeszcze jakieś strony. Oprócz tego zdarzenie PrintPage zawiera dane które zamierzamy wydrukować.

Stwórzmy dla przykładu najprostszą aplikację pozwalającą na wydrukowanie, krótkiej ankiety osobowej:




Na formatkę rzucamy 4 labele i textboxy oraz jeden button (nazwy kontrolek pozostawiłem standardowe tj. label1, textBox1…). Po naciśnięciu na klawisz Drukuj, chcielibyśmy aby nasza drukarka zabrała się do pracy:) (po drodze oczywiście dodamy okienko umożliwiające wybranie która drukarka ma wydrukować dla nas stronę).
Oczywiście nasz interfejs należy oprogramować:



    1         private PrintDocument printDocument;
    2         private PrintDialog printDialog; //Okno dialogowe wyboru drukarki
    3         public Form1()
    4         {
    5             InitializeComponent();
    6         }
    7
    8         private void button1_Click(object sender, EventArgs e)
    9         {
   10             printDocument = new PrintDocument();
   11             printDialog = new PrintDialog();
   12             printDialog.Document = printDocument;
   13             printDocument.PrintPage += new PrintPageEventHandler(printDocument_PrintPage);
   14             printDocument.Print();
   15         }
   16
   17         void printDocument_PrintPage(object sender, PrintPageEventArgs e)
   18         {
   19             Graphics g = e.Graphics;
   20             string textToPrint = string.Format("Imie: {0}\nNazwisko: {1}\nRok urodzenia: {2}\nMiasto: {3}", textBox1.Text, textBox2.Text, textBox3.Text, textBox4.Text);
   21             using (Font font = new Font("Arial", 10))
   22             {
   23                 g.DrawString(textToPrint, font, Brushes.Black, 50, 50);
   24             }
   25         }

Dzięki obiektowi Graphics, niejako "malujemy" to co chcemy zobaczyć na wydrukowanej stronie. 


Całym procesem drukowania zarządza klasa PrintDocument. Dzięki niej możemy zarządzać wszystkimi aspektami drukowania. Spójrzmy na diagram przedstawiający podstawowe właściwości klas zarządzających procesem drukowania:


Oczywiście praktycznie wszystkie te wartości można ustawić w okienku PrintDialog, jednak warto wiedzieć, że można je ustawić także z kodu.

Aby utrwalić zdobytą wiedzę, proponuję stworzyć prostą aplikację, tworzącą raport zarobków pracowników firmy. Dane pobierzemy z pliku txt (można równie dobrze pobrać je z bazy danych). Struktura pliku jest następująca:

Jak widać mamy w naszym pliku kolumny takie jak: imię, nazwisko, wiek oraz zarobki. Interfejs naszej aplikacji może wyglądać tak:

Kod:


    1 using System;
    2 using System.Collections.Generic;
    3 using System.ComponentModel;
    4 using System.Data;
    5 using System.Drawing;
    6 using System.Linq;
    7 using System.Text;
    8 using System.Windows.Forms;
    9
   10 using System.IO;
   11 using System.Drawing.Printing;
   12
   13 namespace WindowsFormsApplication7
   14 {
   15     public partial class Form1 : Form
   16     {
   17         private StreamReader sr;
   18         private float lineHeight;
   19         private PrintDocument pd;
   20
   21         public Form1()
   22         {
   23             InitializeComponent();
   24             pd = new PrintDocument();
   25         }
   26
   27         private void button1_Click(object sender, EventArgs e)
   28         {
   29             using (OpenFileDialog openFile = new OpenFileDialog())
   30             {
   31                 openFile.Filter = "Text Files |*.txt";
   32                 if (openFile.ShowDialog() == DialogResult.OK)
   33                 {
   34                     richTextBox1.LoadFile(openFile.FileName, RichTextBoxStreamType.PlainText);
   35                     sr = new StreamReader(openFile.FileName, Encoding.UTF8);
   36                     textBox1.Text = openFile.FileName;
   37                     button3.Enabled = true;
   38                 }
   39             }
   40
   41         }
   42
   43         private void button3_Click(object sender, EventArgs e)
   44         {
   45             pd.PrintPage += new PrintPageEventHandler(pd_PrintPage);
   46             pd.Print();
   47         }
   48
   49         void pd_PrintPage(object sender, PrintPageEventArgs e)
   50         {
   51             Graphics g = e.Graphics;
   52             int lineWrite = 2;
   53             string actualLine;
   54             string[] printLine;
   55             float y = 0;
   56             using (Font f = new Font("Arial", 10))
   57             using (Font f2 = new Font("Arial",10, FontStyle.Bold))
   58             {
   59                 lineHeight = f.GetHeight(g);
   60                 int linesPerPage = (int)((e.MarginBounds.Bottom - e.MarginBounds.Top) / lineHeight);
   61                 g.DrawString("Raport zarobków", f2, Brushes.Black, new PointF(0, y));
   62                 y = 2 * lineHeight;
   63                 while ((actualLine = sr.ReadLine()) != null)
   64                 {
   65                     printLine = actualLine.Split(';');
   66                     string firstName = printLine[0];
   67                     string lastName = printLine[1];
   68                     int age = int.Parse(printLine[2]);
   69                     decimal wage = decimal.Parse(printLine[3]);
   70                     g.DrawString(firstName, f, Brushes.Black, new PointF(0, y));
   71                     g.DrawString(lastName, f, Brushes.Black, new PointF(100, y));
   72                     g.DrawString(age.ToString(), f, Brushes.Black, new PointF(200, y));
   73                     g.DrawString(wage.ToString(), f, Brushes.Black, new PointF(300, y));
   74                     y += lineHeight;
   75                     lineWrite++;
   76                     if (lineWrite > linesPerPage)
   77                     {
   78                         e.HasMorePages = true;
   79                         break;
   80                     }
   81                 }
   82             }
   83         }
   84     }
   85 }

 Dla przykładu zamieszczam całą aplikację (wraz z kodami źródłowymi) gotową do użycia: pobierz. Odnośnie samego drukowania nie jest ono takie trudne jak się wydaje. Istnieje wiele bibliotek wspomagających ten proces (jedną z popularniejszych jest z pewnością Crystal Reports).

środa, 11 listopada 2009

Wielojęzyczna aplikacja


Tworzenie wielojęzycznych aplikacji w C# z wykorzystaniem Visual Studio jest wręcz tak proste jak deklaracja zmiennej. Jest to możliwe dzięki użyciu zasobów.

Przypuśćmy że mamy napisaną aplikację jak na poniższym screenie:

Jak widać aplikacja pozwala na gromadzenie kilku informacji o osobach. Informacje te później można by zapisywać do bazy itp. Jednak nie chodzi tutaj o samą koncepcję budowania aplikacji do przechowywania informacji o osobach, a zbudowanie aplikacji wielojęzycznej.
Kod który został użyty przy budowie aplikacji:
    1         private void button1_Click(object sender, EventArgs e)
    2         {
    3             Person p = new Person(textBox1.Text, textBox2.Text, int.Parse(textBox3.Text));
    4             listBox1.Items.Add(p);
    5         }
    6
    7         private void button2_Click(object sender, EventArgs e)
    8         {
    9             System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en");
   10         }
   11     }
   12
   13     class Person
   14     {
   15         public string Imie { get; private set; }
   16         public string Nazwisko { get; private set; }
   17         public int Wiek { get; private set; }
   18
   19         public Person(string imie, string nazwisko, int wiek)
   20         {
   21             this.Imie = imie;
   22             this.Nazwisko = nazwisko;
   23             this.Wiek = wiek;
   24         }
   25
   26         public override string ToString()
   27         {
   28             return string.Format("{0} {1} {2}", Imie, Nazwisko, Wiek);
   29         }
   30     }



Aby uczynić naszą aplikację wielojęzyczną na początek zmieniamy właściwość Localizable naszej formy na true. Powoduje to wpisanie właściwości wszystkich kontrolek na formie do pliku zasobów.
Teraz ustawiamy Właściwość Language na taki język jak nas interesuje i edytujemy właściwości kontrolek (przeważnie zmieniamy tylko właściwość Text). Dla każdego wybranego języka jest tworzony nowy plik zasobów. Środowisko samo wybiera odpowiedni plik zasobów na podstawie ustawień systemowych.

Ja wykorzystałem dwa języki polski i angielski:



W zależności od ustawień systemu, zostanie wybrany odpowiedni plik językowy. Warto zauważyć że dla każdego języka tworzony jest nowy plik z zasobami co pokazuje poniższy screen:

Dzięki takiemu zastosowaniu zasobów w prosty sposób możemy stworzyć wielojęzyczną aplikację.

Drag & Drop

Ciężko w dzisiejszych czasach wyobrazić sobie można aplikacji, która nie zawiera w sobie elementów Drag and Drop (przeciągnij i upuść). Implementacja tego zadania dla kontrolek w WF nie jest trudna. Aby zrozumieć mechanizm łatwego „przerzucania” elementów z jednej kontrolki do drugiej, warto zapoznać się z procesami, które zachodzą w momencie takiej operacji. Całość omówimy na przykładzie dwóch kontrolek ListBox.
Jak widać na screenie, na formatce położyłem dwa labele oraz dwie kontrolki ListBox (listBox1 i listBox2). ListBox1 zawiera elementy które będziemy chcieli umieścić w drugim ListBoxie. Tak więc możemy się umówić że na listBox1 będziemy mówili źródłowy, a listBox2 docelowy.
Zacznijmy od kontrolki źródłowej. Drag & Drop rozpoczyna się przeważnie od „chwycenia myszką” zawartości kontrolki. Jak więc nie trudno się domyślić będziemy musieli obsłużyć zdarzenie MouseDown. W tym momencie kontrolka źródłowa powinna zainicjować operację przenoszenia elementu. Do wykonania tej części będzie konieczne wywołanie metody DoDragDrop. Metoda ta posiada dwa parametry: data typu object (czyli dane które chcemy przenieść) oraz typ wyliczeniowy DragDropEffects. W typie wyliczeniowym możemy znaleźć takie opcje jak:
None – kontrolka docelowa nie pozwala na kopiowanie
Copy – powoduje skopiowanie danych z kontrolki źródłowej do docelowej
Move – przenosi dane z kontrolki źródłowej do docelowej
Link – zostaje utworzone połączenie (logiczne) z elementem docelowym
Scroll – pozwala na przewijanie kontrolki docelowej podczas przenoszenia
All – połączenie efektów Copy, Move i Scroll
Jak więc widać mamy do dyspozycji wiele ciekawych efektów.
To wszystko jeśli chodzi o odpowiedzialność kontrolki źródłowej. Przejdźmy do kontrolki docelowej. Aby możliwe było upuszczenie na docelową kontrolkę jakiegoś elementu, kontrolka musi obsłużyć dwa zdarzenia: DragEnter oraz DragDrop. Zdarzenia te otrzymują parametr DragEventArgs, który zawiera informacje wymagane do wykonania procedury przenoszenia.
Spójrzmy na elementy składające się na DragEventArgs:
AllowedEffect – efekt który jest wspierany przez kontrolkę źródłową
Data – zwraca obiekt typu IDataObject w którym przechowywane są dane z kontrolki źródłowej
Effect – ustala efekt dla kontrolki docelowej
X,Y – współrzędne myszy
KeyState – zwraca stan klawiszy i myszy jako integer:
1 – wciśnięty lewy przycisk myszy
2 – wciśnięty prawy przycisk myszy
5 – wciśnięty klawisz Shift
9 – wciśnięty klawisz Ctrl
16 –wciśnięty środkowy przycisk myszy
32 – wciśnięty przycisk Alt
 
A więc zbierając wszystkie informacje:
W zdarzeniu DragEnter:
 - Za pomocą metody Data.GetDataPresent sprawdzamy czy przekazujemy poprawny typ danych do     kontrolki docelowej (np. łańcuch znakowy)
- właściwości Effect używamy aby powiadomić kontrolkę źródłową w jaki sposób kontrolka docelowa odbierze dane
W zdarzeniu DragDrop:
- za pomocą Data.DetData dostajemy się do danych które biorą udział w całym procesie
Przejdźmy teraz może do przykładu. W przykładzie tym spowodujemy, że przy przenoszeniu normalnym dane będą przenoszone, a przy wciśniętym dodatkowo klawiszu Shift kopiowane:
    1         /// <summary>
    2         /// Obsługa zdarzenia MoudeDown dla kontrolki źródłowej
    3         /// </summary>
    4         /// <param name="sender"></param>
    5         /// <param name="e"></param>
    6         private void listBox1_MouseDown(object sender, MouseEventArgs e)
    7         {
    8             //sprawdzamy czy przeciągamy element zawierający jakąkolwiek treść
    9             if (listBox1.SelectedIndex >= 0 && listBox1.SelectedItem.ToString() != "")
   10             {
   11                 //zapamiętujemy index
   12                 int index = listBox1.SelectedIndex;
   13                 DragDropEffects effect;
   14                 effect = DoDragDrop(listBox1.Items[index].ToString(), DragDropEffects.Move | DragDropEffects.Copy);
   15                 //w przypadku przenoszenia kasujemy obiekt z listy
   16                 //w przypadku kopiowania obiekt pozostanie na liście
   17                 if (effect == DragDropEffects.Move)
   18                 {
   19                     listBox1.Items.RemoveAt(index);
   20                 }
   21             }
   22
   23         }
   24
   25         /// <summary>
   26         /// Obsługa zdarzenia DragEnter kontrolki docelowej
   27         /// </summary>
   28         /// <param name="sender"></param>
   29         /// <param name="e"></param>
   30         private void listBox2_DragEnter(object sender, DragEventArgs e)
   31         {
   32             //sprawdzamy czy typy się zgadzają, jeśli nie zabraniamy przenoszenia
   33             if (e.Data.GetDataPresent(typeof(string)))
   34             {
   35                 if ((e.KeyState & 4) == 4)
   36                 {
   37                     e.Effect = DragDropEffects.Copy;
   38                 }
   39                 else
   40                 {
   41                     e.Effect = DragDropEffects.Move;
   42                 }
   43             }
   44             else
   45             {
   46                 e.Effect = DragDropEffects.None;
   47             }
   48         }
   49
   50         /// <summary>
   51         /// Obsługa zdarzenia DragDrop kontrolki docelowej
   52         /// </summary>
   53         /// <param name="sender"></param>
   54         /// <param name="e"></param>
   55         private void listBox2_DragDrop(object sender, DragEventArgs e)
   56         {
   57             //dodajemy nowy element do listy
   58             string newItem = e.Data.GetData(typeof(string)).ToString();
   59             listBox2.Items.Add(newItem);
   60         }
Podsumowując, aby zaimplementować Drag & Drop musieliśmy przejść następujące etapy:
 
To na tyle jeśli chodzi o implementację Drag & Drop. Oczywiście powyższy kod to tylko przykład. Można by jeszcze dodać obsługę dwustronną przeciągania itp. Oprócz tekstu można przeciągać także inne elementy jak np. grafikę. Myślę że teraz implementacja Drag & Drop nie sprawi nikomu trudności. Powodzenia i udanych eksperymentów.