ś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).

sobota, 14 listopada 2009

Kontrolka w kontrolce, czyli o Content w WPF

Zapewnie wielu z was myślało o zwariowanych pomysłach umieszczenia w np. Buttonie TextBoxa. Dzięki nowemu modelowi, który wprowadzono w WPF takie sztuczki są możliwe. Prawie każda kontrolka w WPF dziedziczy po klasie ContentControl. Dzięki temu kontrolki uzyskują możliwość przechowywania w sobie innych kontrolek. Najłatwiej zobaczyć to na przykładzie. Wspominałem na początku o możliwości umieszczenia w Buttonie kontrolki TextBox. Spróbujmy więc osiągnąć taki efekt:


Kod potrzebny aby wygenerować taki efekt jest niezwykle prosty:
   21         <Button>
   22             <TextBox MinWidth="50"></TextBox>
   23         </Button>

Oczywiście w Buttonie możemy umieścić inne rzeczy jak np. jakiś obrazek, TextBlock itd.

Przyjrzyjmy się teraz kontrolką, które mogą przyjąć tylko jedną kontrolkę.

ScrollViewer
W WPF nie ma innego kontenera który umożliwia przewijanie swojej zawartości w tak prosty sposób jaki to robi ScrollViewer. Jako wewnętrzny kontener, stosujemy przeważnie dla niego Grid. Przykład:

 Efekt ten uzyskałem dzięki położeniu w ScrollViewerze Grida, a w nim 9 wierszy i 9 przycisków. Kod:

    6         <ScrollViewer>
    7             <Grid>
    8                 <Grid.RowDefinitions>
    9
   10                     <RowDefinition MinHeight="25"></RowDefinition>
   11                     <RowDefinition MinHeight="25"></RowDefinition>
   12                     <RowDefinition MinHeight="25"></RowDefinition>
   13                     <RowDefinition MinHeight="25"></RowDefinition>
   14                     <RowDefinition MinHeight="25"></RowDefinition>
   15                     <RowDefinition MinHeight="25"></RowDefinition>
   16                     <RowDefinition MinHeight="25"></RowDefinition>
   17                     <RowDefinition MinHeight="25"></RowDefinition>
   18                     <RowDefinition MinHeight="25"></RowDefinition>
   19                     <RowDefinition MinHeight="25"></RowDefinition>
   20                 </Grid.RowDefinitions>
   21                 <Button Grid.Row="0"></Button>
   22                 <Button Grid.Row="1"></Button>
   23                 <Button Grid.Row="2"></Button>
   24                 <Button Grid.Row="3"></Button>
   25                 <Button Grid.Row="4"></Button>
   26                 <Button Grid.Row="5"></Button>
   27                 <Button Grid.Row="6"></Button>
   28                 <Button Grid.Row="7"></Button>
   29                 <Button Grid.Row="8"></Button>
   30                 <Button Grid.Row="9"></Button>
   31             </Grid>
   32         </ScrollViewer>


Scroll jest aktywny w momencie gdy jest zbyt mało miejsca aby wyświetlić zawartość Grida. Gdy rozciągniemy okno Scroll przestaje być aktywny.

GroupBox
Reprezentuje prosty prostokąt z tytułem. Można w nim zgrupować kontrolki które np służą do wybierania jakichś opcji. Przykład:

Kod aby taki efekt uzyskać:

 
    5     <Grid>
    6         <Grid.RowDefinitions>
    7             <RowDefinition></RowDefinition>
    8             <RowDefinition></RowDefinition>
    9             <RowDefinition></RowDefinition>
   10         </Grid.RowDefinitions>
   11         <Grid.ColumnDefinitions>
   12             <ColumnDefinition></ColumnDefinition>
   13             <ColumnDefinition></ColumnDefinition>
   14             <ColumnDefinition></ColumnDefinition>
   15         </Grid.ColumnDefinitions>
   16         <GroupBox Grid.Column="1" Grid.Row="1" Header="Opcje">
   17             <StackPanel>
   18                 <CheckBox>
   19                     <CheckBox.Margin>
   20                         <Thickness Top="4" Bottom="4"></Thickness>
   21                     </CheckBox.Margin>
   22                     Opcja pierwasz
   23                 </CheckBox>
   24                 <CheckBox>
   25                     <CheckBox.Margin>
   26                         <Thickness Top="4" Bottom="4"></Thickness>
   27                     </CheckBox.Margin>
   28                     Opcja druga
   29                 </CheckBox>
   30                 <CheckBox>
   31                     <CheckBox.Margin>
   32                         <Thickness Top="4" Bottom="4"></Thickness>
   33                     </CheckBox.Margin>
   34                     Opcja trzecia
   35                 </CheckBox>
   36                 <Button>
   37                     <Button.Margin>
   38                         <Thickness Top="20"></Thickness>
   39                     </Button.Margin>
   40                     Ok
   41                 </Button>
   42             </StackPanel>
   43         </GroupBox>
   44     </Grid>

TabItem
TabItem reprezentuje pojedynczą stronę w kontrolce TabControl. Za pomocą tej kontrolki można uzyskać modne w dzisiejszych aplikacjach zakładki:

 Kod dzięki któremu uzyskamy taki efekt:
 

    5     <Grid>
    6         <TabControl>
    7             <TabItem Header="Opcje czcionki">
    8                 <StackPanel>
    9                     <Button>Kliknij mnie</Button>
   10                     <CheckBox>Zaznacz mnie</CheckBox>
   11                     <RadioButton>Wybierz mnie</RadioButton>
   12                 </StackPanel>
   13             </TabItem>
   14             <TabItem Header="Opcje wideo"></TabItem>
   15             <TabItem Header="Opcje dzwiku"></TabItem>
   16         </TabControl>
   17     </Grid>

Expander
Pozwala użytkownikowi na ukrywanie/otwieranie jego zawartości poprzez klikanie na małą strzałkę. Najlepiej wyjaśni to przykład:

 Kod:

    5     <StackPanel>
    6         <Expander Grid.Row="0" Background="Azure">
    7             <StackPanel>
    8                 <Button>Przycisk</Button>
    9                 <RadioButton>RadioButton</RadioButton>
   10                 <CheckBox>CheckBox</CheckBox>
   11             </StackPanel>
   12         </Expander>
   13         <Expander Grid.Row="1" Background="BlanchedAlmond"></Expander>
   14     </StackPanel>

Dekoratory
Border - czyli najprościej ujmując kolor brzegu. Zawiera takie właściwości jak Background (kolor tła), BorderBrush i BorderThickness (kolor brzegu i jego grubość), CornerRadius (zaokrąglenie brzegów), Padding (dodaje przestrzeń pomiędzy brzegiem a zawartością kontrolki) .

Vievbox - umożliwia przeskalowywanie grafiki zawartej w nim (najlepiej do tego nadaje się oczywiście grafika wektorowa); rzadko przechowuje się w nim inne kontrolki.

Tyle na dziś. W następnej części kursu poznamy specjalny rodzaj zdarzeń i właściwości wykorzystywanych w WPF.

ś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.