czwartek, 22 października 2009

Grafika, czyli GDI+ w pigułce

.NET Framework umożliwia w prosty sposób manipulowanie grafiką. Dodając do naszego projektu przestrzeń nazw System.Drawing możemy między innymi:
- rysować różne elementy graficzne (linie, koła itp.) w sposób dynamiczny
- edytować obrazy
- przycinać i powiększać zdjęcia
- malować łańcuchy znakowe (np. informacje o autorze czy znak wodny)

To oczywiście tylko niektóre z możliwości, których nam dostarcza ta biblioteka.

Przestrzeń Graphics zawiera wiele klas i struktur, które będziemy wykorzystywać podczas dalszej pracy. Na początek wymienię kilka klas, które z pewnością niejednokrotnie wykorzystamy:
Bitmap – wykorzystujemy ją podczas ładowania i zapisywania obrazów
Brush – pozwala na wypełnianie określonej przestrzeni (np. wnętrze narysowanego koła jakimś kolorem)
Brushes – jeśli nie chcemy tworzyć instancji klasy Brush, możemy skorzystać z tej klasy
Font – reprezentuje łańcuch tekstowy, pozwala na określenie atrybutów malowanego tekstu
Graphics – używamy tej klasy gdy chcemy coś namalować, dodać tekst do zdjęcia itp.
Icon – reprezentuje ikonę
Image – klasa bazowa dla klas Bitmap i Metafile
Pen – klasa reprezentująca narzędzie do malowania linii i innych kształtów graficznych
Pens – jeśli nie mamy zamiaru tworzyć instancji Pen, możemy skorzystać z tej klasy

A teraz kilka przydatnych struktur:
Color – reprezentuje kolor
Point – współrzędne X i Y punktu
PointF – współrzędne X i Y punktu (reprezentowane jako wartości float)
Rectangle i RectangleF – reprezentują prostokąt (wersja z wartościami int i float)
Size i SizeF – przechowuje wysokość i szerokość (przeważnie wykorzystywane dla prostokąta)

Z tym zestawem przydatnych klas i struktur możemy rozpocząć naszą przygodę z grafiką.

 Malowanie linii i kształtów

Aby malować po kontrolce czy też formie musimy wykonać następujące kroki:
1.       Tworzymy obiekt Graphics wywołując metodę Control.CreateGraphics()
2.       Tworzymy obiekt klasy Pen (możemy także wykorzystać tu także klasę Pens)
3.       Korzystając z metod klasy Graphics, rysujemy na naszej kontrolce
Dla przykładu namalujemy coś prostego:

Kod:

   29             using (Graphics g = this.CreateGraphics())
   30             using (Pen pen = new Pen(Color.Black, 4))
   31             {
   32                 g.Clear(this.BackColor);
   33                 g.DrawEllipse(pen, 100, 50, 200, 300);
   34                 g.DrawEllipse(pen, new Rectangle(140, 110, 30, 50));
   35                 g.DrawEllipse(pen, new Rectangle(200, 110, 30, 50));
   36                 g.FillEllipse(Brushes.Black, new Rectangle(160, 130, 10, 20));
   37                 g.FillEllipse(Brushes.Black, new Rectangle(220, 130, 10, 20));
   38                 g.DrawLine(pen, 170, 180, 140, 220);
   39                 Point[] points =
   40                 {
   41                     new Point(160, 280),
   42                     new Point(300, 300),
   43                     new Point(250,300),
   44                     new Point(260, 280)
   45                 };
   46                 g.DrawBeziers(pen, points);
   47             }
Jak widać malowanie różnych kształtów nie jest trudne :)

 Praca z obrazami

Praca z obrazami jest możliwa dzięki jednej z klas: Image, Bitmap lub Metafile. Klasa Image jest klasą abstrakcyjną i zarazem bazową dla dwóch pozostałych. Obiekt tej klasy utworzyć możemy korzystając z jednej z jej metod (np. Image.FromFile(sciezka_do_pliku)). Po utworzeniu obiektu tej klasy możemy nasz obraz zmniejszyć, zapisać w innym formacie, dodać tekst czy też znak wodny.
Klasa Bitmap, dziedzicząca po Image, rozwija wachlarz nowych możliwości pracy z obrazem. Posiada dwie właściwości, które szczególnie mogą się nam przydać przy pracy z plikami graficznymi:
GetPixel  - zwraca pojedyńczy piksel, a dokładniej obiekt typu Color, który zawieta informację na temat koloru którym został namalowany ten piksel
SetPixel – pozwala na ustawienie koloru wybranego piksela

Teraz kilka praktycznych czynności z którymi możemy się spotkać podczas pracy z plikami graficznymi:
1.       Otwieranie obrazka w PictureBox:
        private void button1_Click(object sender, EventArgs e)
        {
            OpenFileDialog openFileDialog = new OpenFileDialog();
            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                pictureBox1.Image = Image.FromFile(openFileDialog.FileName);
            }
        }
2.       Zapisywanie do pliku:
            SaveFileDialog saveFileDialog = new SaveFileDialog();
            if (saveFileDialog.ShowDialog() == DialogResult.OK)
            {
                pictureBox1.Image.Save(saveFileDialog.FileName, ImageFormat.Png);
            }

3.       Zmniejszanie powiększanie grafiki:
            Rectangle rectangle = new Rectangle(pictureBox1.Location, new Size(100, 100));
            Graphics g = pictureBox1.CreateGraphics();
            g.Clear(pictureBox1.BackColor);
            g.DrawImage(pictureBox1.Image, rectangle);


Praca z tekstem

Aby dodać tekst do grafiki należy wykonać następujące czynności:
1.       Stworzyć obiekt Graphics
2.       Stworzyć obiekt Font
3.       Wybrać jeden z predefiniowanych pędzli (Brushes) lub stworzyć nowy (Brush)
4.       Wywołać metodę Graphics.DrawString() przekazując tekst do namalowania oraz jego położenie

Przykład umieszczenia tekstu na obrazku znajdującym się w PictureBox:
            Font newFont = new Font(new FontFamily("Arial"), 12, FontStyle.Bold);
            Graphics g = pictureBox1.CreateGraphics();
            g.DrawString("Ala ma kota", newFont, Brushes.BlueViolet, new PointF(10,10));

To tyle jeśli chodzi o podstawy GDI+. Reszta, zależy już tylko wyłączenie od naszej wyobraźni i inwencji twórczej.

sobota, 17 października 2009

Krótkie wprowadzenie do ADO.NET


ADO.NET
ADO.NET to zbiór bibliotek pozwalających na komunikację ze źródłami danych (np. baza danych, plik txt, XML, excel itp.). Aby można było pobierać/wysyłać dane do odpowiedniego źródła należy użyć providera. Każdy provider specjalizuje się w obsłudze innego źródła danych:
SQL Server .NET Data Provider – SQL Server
OLE DB .NET Data Provider - Access , Excel
ODBC .NET Data Provider – stare bazy danych
.NET Data Provider for Oracle – dla baz Oracla

Wszystkie tutaj zawarte przykłady opierają się na bazie SQL Server, jednak użycie innych providerów/źródeł danych jest bardzo podobne.

Obiekty w ADO.NET
SqlConnection – przechowuje nazwę, hasło i użytkownika bazy danych jak i ścieżkę do samej bazy danych
SqlCommand – przekazuje polecenia SQL do bazy danych; dzięki obiektowi SqlConnection wie do jakiej bazy danych się odwoływać
SqlDataReader – pozwala tylko na odczyt danych z bazy danych jako strumienia
DataSet – reprezentuje w pamięci odczytane dane. Zawiera w sobie obiekty typu DataTable, które z kolei składają się z obiektów DataRow i DataColumn
SqlDataAdapter – podczas odczytu wypełnia obiekt DataSet. Przechowuje referencję do obiektu SqlConnection, dzięki czemu otwiera/zamyka połączenie kiedy jest to wymagane.

ADO.NET wprowadza dwa tryby łączenia się z danymi: połączeniowy i bezpołączeniowy. Omówimy najpierw tryb połączeniowy.

Tworzenie połączenia z bazą danych:
SqlConnection sqlConnection = new SqlConnection(@"Data Source=(local);
                Initial Catalog = nazwa_bazy_danych; Integrated Security = SSIP");
Data Source – identyfikator serwera; lokalna maszyna, adres ip, domena itd.


Uwaga: Jeżeli korzystamy z Visual Studio w bardzo łatwy sposób możemy wydobyć ConnectionString.
W tym celu należy:
1.       Utworzyć nowe połączenie z bazą danych na zakładce Server Explorer
2.       Wybrać żądaną bazę danych
3.       Kliknąć na Propeteries a następnie skopiować właściwość ConnectionString


Typowy cykl życia połączenia:
1.       Utworzenie obiektu SqlCommand
2.       Otwarcie połączenia
3.        Przekazanie połączenia do innych obiektów biblioteki ADO.NET
4.       Wykonanie operacji za pomocą obiektów ADO.NET w bazie danych
5.       Zamknięcie połączenia

Przykład:
   14 SqlConnection sqlConnection = new SqlConnection(@"Data Source=ACER5738\sqlexpress;Initial Catalog=Szkola;Integrated Security=True");
   15             SqlDataReader dataReader = null;
   16             try
   17             {
   18                 sqlConnection.Open();
   19                 SqlCommand command = new SqlCommand("SELECT * FROM Nauczyciel", sqlConnection);
   20                 dataReader = command.ExecuteReader();
   21                 while (dataReader.Read())
   22                 {
   23                     Console.WriteLine(string.Format("{0} {1} {2}", dataReader[0], dataReader[1], dataReader[2]));
   24                 }
   25             }
   26             finally
   27             {
   28                 if (dataReader!=null)
   29                 {
   30                     dataReader.Close();
   31                 }
   32                 if (sqlConnection != null)
   33                 {
   34                     sqlConnection.Close();
   35                 }
   36             }

 Wstawianie, usuwanie i modyfikacja danych za pomocą SqlCommand
Pobieranie danych już widzieliśmy, teraz czas na wstawianie:

1.       Tworzymy komendę wstawiania np.:
string sqlInsert = @"INSERT INTO Nauczyciel VALUES('85632798654','Jan','Machurski')";
2.       Tworzymy obiekt SqlCommand:
SqlCommand comand = new SqlCommand(sqlInsert, sqlConnection);
3.       Wywołujemy metodę ExecuteNonQuery();
comand.ExecuteNonQuery();


Usuwanie i modyfikacja dokładnie tak jak poprzednio z tym, że oczywiście zmieniamy komendę.

Jak wcześniej było wspomniane jest to tryb połączeniowy z bazą danych. Dlaczego połączeniowy? Ponieważ aby dane otrzymać należy jawnie wykonać komendę Open() na podanym źródle danych.

Teraz omówię pokrótce bezpołączeniowy model. Model ten nie wymaga otwartego połączenia do źródła danych. Oczywiście ConnectionString nadal jest wymagany, jednak co do czasu otwarcia/zamknięcia połączenia jest to decydowane przez obiekt DataAdapter.


DataSet – pozwala na przechowywanie w pamięci danych w postaci tabel.
Korzystając z obiektu DataAdapter w łatwy sposób można pobrać dane z bazy danych. Wykonywane są wtedy następujące czynności:
1.       Otwarcie połączenia
2.       Wypełnienie danymi obiektu DataSet
3.       Zamknięcie połączenia

Podczas uaktualniania mamy do czynienia z:
1.       Otwarcie, połączenia
2.       Zapisaniem zmian z DataSeta do źródła
3.       Zamknięciem połączenia

Przykład:
            SqlConnection sqlConnection = new SqlConnection(@"Data Source=ACER5738\sqlexpress;Initial Catalog=Szkola;Integrated Security=True");
            DataSet dataSet = new DataSet();
            SqlDataAdapter dataAdapter = new SqlDataAdapter("SELECT * FROM Nauczyciel", sqlConnection);
            dataAdapter.Fill(dataSet, "Tabela");
            foreach (DataRow item in dataSet.Tables["Tabela"].Rows)
            {
                Console.WriteLine(item[0] + " " + item[1] + " " + item[2]);
            }

Komendy odpowiedzialne za uaktualnianie/usuwanie czy też dodawanie danych można dodać do obiektu DataAdapter na dwa sposoby:
- poprzez właściwości
- poprzez obiekt SqlCommandBuilder

Sposób drugi jest bardzo prosty i nie wymaga zbytniej wiedzy nawet na temat SQL-owych komend UPDATE/DELETE. Wystarczy utworzyć obiekt SqlCommandBuilder i w jego konstruktorze przekazać referencję do obiektu SqlDataAdapter. Jak jednak można się domyślić jest tu mały haczyk. SqlCommandBuilder nie działa podczas łączenia wielu tabel oraz przy pracy z procedurami. Także wydajność zapytań SQL-owcych może budzić wątpliwości. Jednak podczas pracy z pojedynczymi tabelami jest jak najbardziej wskazany.

Aby zapisać dane (np. po modyfikacji) z DataSetu z powrotem do źródła należy użyć komendy Update() DataAdaptera:
dataAdapter.Update(dataSet, "nazwa_uaktualnianej_tabeli");

Dodawanie parametrów do zapytań SQL
Parametry, które dodajemy do zapytania SQL (np. tworząc warunek wyszukiwania imienia które poda użytkownik). Aby stworzyć poprawne zapytanie SQL:
1.       Tworzymy łańcuch SqlCommand z parametrami
2.       Deklarujemy obiekt SqlParametr z odpowiednimi wartościami
3.       Przypisujemy SqlParametr do SqlCommand poprzez właściwość Parameters
Pzykład:
            SqlConnection sqlConnection = new SqlConnection(@"Data Source=ACER5738\sqlexpress;Initial Catalog=Szkola;Integrated Security=True");
            SqlCommand command = new SqlCommand("SELECT * FROM Nauczyciel WHERE imie = @imie", sqlConnection);
            SqlParameter parametr = new SqlParameter("@imie", "Jan");
            command.Parameters.Add(parametr);
            sqlConnection.Open();
            SqlDataReader dataReader = command.ExecuteReader();
            while (dataReader.Read())
            {
                Console.WriteLine(dataReader[0] + " " + dataReader[1] + " " + dataReader[2]);
            }
            sqlConnection.Close();


Używanie procedur i funkcji zapisanych w bazie danych
Aby było możliwe używanie procedur/funkcji zapisanych w bazie danych, należy kolejno:
1.       W obiekcie SqlCommand przekazać nazwę funkcji/procedury do wywołania
2.       SqlCommand musi wiedzieć, że wywołuje procedurę (domyślnie ustawione jest wywołanie zapytania SQL)
Przykład:
            SqlConnection sqlConnection = new SqlConnection(@"Data Source=ACER5738\sqlexpress;Initial Catalog=Szkola;Integrated Security=True");
            SqlCommand command = new SqlCommand("SprawdzPesele", sqlConnection);
            command.CommandType = CommandType.StoredProcedure;

Na koniec jeszcze pokaże jak bindować SqlDataReader z DataGridView:
            SqlCommand command = new SqlCommand("SELECT * FROM Nauczyciel", connection);
            SqlDataReader dataReader = command.ExecuteReader();
            BindingSource bindingSource = new BindingSource();
            bindingSource.DataSource = dataReader;
            dataGridView1.DataSource = bindingSource;


Na razie tyle odnośnie używania ADO.NET. Po tym krótkim wprowadzeniu, proste czynności związane z obsługą źródeł danych nie powinny stanowić problemu. Powodzenia.

poniedziałek, 12 października 2009

Układ elementów na formie - część 2

Wracając do tematu kontenerów na nasze kontrolki, zanim opisze jeszcze inne, kilka aspektów Grida o których jeszcze nie wspominałem.

Elementy które tworzymy w Gridzie możemy przykładowo rozciągnąć na kilka komórek czy kolumn. Przykładowo utworzony przycisk:

    5     <Grid>
    6         <Grid.RowDefinitions>
    7             <RowDefinition></RowDefinition>
    8             <RowDefinition></RowDefinition>
    9             <RowDefinition></RowDefinition>
   10             <RowDefinition></RowDefinition>
   11             <RowDefinition></RowDefinition>
   12             <RowDefinition></RowDefinition>
   13             <RowDefinition></RowDefinition>
   14         </Grid.RowDefinitions>
   15         <Grid.ColumnDefinitions>
   16             <ColumnDefinition></ColumnDefinition>
   17             <ColumnDefinition></ColumnDefinition>
   18             <ColumnDefinition></ColumnDefinition>
   19             <ColumnDefinition></ColumnDefinition>
   20             <ColumnDefinition></ColumnDefinition>
   21         </Grid.ColumnDefinitions>
   22         <Button Grid.Row="1" Grid.Column="1" Content="Klik"></Button>
   23     </Grid>

Możemy rozciągnąć na sąsiadującą komórkę i kolejny wiersz:


    5     <Grid>
    6         <Grid.RowDefinitions>
    7             <RowDefinition></RowDefinition>
    8             <RowDefinition></RowDefinition>
    9             <RowDefinition></RowDefinition>
   10             <RowDefinition></RowDefinition>
   11             <RowDefinition></RowDefinition>
   12             <RowDefinition></RowDefinition>
   13             <RowDefinition></RowDefinition>
   14         </Grid.RowDefinitions>
   15         <Grid.ColumnDefinitions>
   16             <ColumnDefinition></ColumnDefinition>
   17             <ColumnDefinition></ColumnDefinition>
   18             <ColumnDefinition></ColumnDefinition>
   19             <ColumnDefinition></ColumnDefinition>
   20             <ColumnDefinition></ColumnDefinition>
   21         </Grid.ColumnDefinitions>
   22         <Button Grid.Row="1" Grid.Column="1" Content="Klik" Grid.ColumnSpan="2" Grid.RowSpan="2"></Button>
   23     </Grid>

Grid posiada tzw. GridSplitter. Dzięki niemu w łatwy sposób jesteśmy w stanie podzielić okno na części i powiększać/zmniejszać je. Używanie GridSplittera może okazać się na początku mało intuicyjne, jednak nic mylnego. GridSplittera możemy umieścić w komórce razem z inną zawartością, jednak w większości przypadków rezerwuje się dla niego osobną kolumnę lub wiersz z wysokością lub szerokością ustawioną na Auto. Podczas zmiany szerokości/wysokości zmieniamy szerokość/wysokość nie pojedynczej komórki, a całej kolumny/wiersza. Należy też nadać jakąś szerokość/wysokość GridSplitterowi, gdyż w przeciwnym razie będzie niezwykle mały i nie poręczny. W zależności od tego jaki chcemy uzyskać efekt przy zmianie wartości szerokości czy też wysokości wierszy, należy odpowiednio ustawić VerticalAligment lub HorizontalAligment na jedną z wartości (jeśli ustawimy Center to mamy możliwość zmniejszania/zwiększania w obie strony).

To tyle jeżeli chodzi o Grid. Jest to jedna z najbardziej rozbudowanych i uniwersalnych kontrolek w WPF.

Canvas
Canas jest jedynym kontenerem który przechowuje położone na nim elementy zapamiętując ich położenie względem lewej i górnej krawędzi (alternatywnie można zapamiętywać prawą i dolną krawędź). Nie jest to najlepsze rozwiązanie jeżeli chodzi o wszechstronność aplikacji.

InkCanvas
Podobnie jak Canvas kontrolki położone na InkCanvas zapamiętywane są na formatce poprzez koordynaty. Zasada działania tego kontenera jest podobna jest do używania tabletu graficznego. W zależności od wybranego trybu możemy malować po nim lub przeciągać elementy.
Najpopularniejsze wykorzystanie, czyli jako podkład do malowania został przedstawiony na poniższym screenie:


Kod:
    1     <InkCanvas EditingMode="Ink">
    2
    3     </InkCanvas>

Zmieniając właściwość EditingMode można uzyskać inne efety:
Ink - domyślny tryb, pozwala na malowanie
GestureOnly - pozwala wykorzytsać gesty myszy (pełen spis znaleźć można w typie wyliczeniowym System.Windows.Ink.ApplicationGesture)
InkAndGesture - połączenie trybu Ink i GestureOnly
EraseByStroke - wymazywanie narysowanego całego kształtu (wymazuje kształt narysowany "jednym pociągnięciem)
EraseByPoint - wymazuje konkretny punkt
Select - pozwala na zaznaczanie elementów przechowywanych w InkCanvas. Zaznaczanie odbywa się poprzez zarzucenie "lassa" na wybrany element (podobnie jak w paincie). Zaznaczony element można zmniejszać/zwiększać, przesuwać, usunąć.
None - zmienia InkCanvas w zwyczajny Canvas


Tyle jeśli chodzi o kontenery mogące przechowywać kontrolki w WPF. W następnej części przyjrzymy się zawartości kontrolek.

Decorator Pattern

Wzorzec ten pozwala na dodawanie nowej funkcjonalności dynamicznie (podczas działania programu). Jest też elastyczną alternatywą dla dziedziczenia. Wzorzec ten wykorzystuje mechanizm wraperów (opakowań).

ConcreteComponent - klasa którą zamierzamy "udekorować"
 Decorator - klasa abstrakcyjna po której dziedziczą konkretne dekoratory, powiększające "koszyk" możliwości klasy ConcreteComponent


Aby lepiej sobie całą sytuację wyobrazić rozważmy sobie przykład. Mamy klasę Kawa. po klasie Kawa może dziedziczyć np kawa zbożowa, espresso, java itd. Do każdej kawy można dodać cukier, mleko czy też mokkę (gatunek kawy arabskiej). Tak więc w naszym założeniu moglibyśmy do naszych klas np. dodać 3 właściwości bool o nazwach: mleko, cukier, mokka. Jednak teraz wyobraźmy sobie, że doszedł nam nowy rodzaj kawy oraz nowy dodatek (śmietanka). Musimy przejść przez wszystkie nasze klasy i dodać (rozszerzyć) odpowiednie funkcje dzięki którym będziemy wiedzieli z czym pijemy naszą kawę.

Aby uniknąć takiej sytuacji warto użyć w tym miejscu wzorca projektowego Decorator. Pozwala on na bardzo łatwe zwiększanie funkcjonalności podstawowych klas. Wracając do naszego przykładu z kawą:

I kod:

    8     public abstract class Coffe
    9     {
   10         protected string description = "Unknow Coffe";
   11
   12         public virtual string GetDescription()
   13         {
   14             return description;
   15         }
   16
   17         public virtual double Cost()
   18         {
   19             return 0;
   20         }
   21     }

    8     public class Jawajska : Coffe
    9     {
   10         public override double Cost()
   11         {
   12             return 2.0;
   13         }
   14         public override string GetDescription()
   15         {
   16             return "Jawajska";
   17         }
   18     }

    8     public abstract class CondimentsDecorator : Coffe
    9     {
   10         protected Coffe coffe;
   11     }

    8     public class Milk : CondimentsDecorator
    9     {
   10         
   11
   12         public Milk(Coffe coffe)
   13         {
   14             this.coffe = coffe;
   15         }
   16
   17         public override double Cost()
   18         {
   19             return 0.2 + coffe.Cost();
   20         }
   21
   22         public override string GetDescription()
   23         {
   24             return coffe.GetDescription() + ", Milk";
   25         }
   26     }

    8     public class Mokka : CondimentsDecorator
    9     {
   10      
   11         public Mokka(Coffe coffe)
   12         {
   13             this.coffe = coffe;
   14         }
   15
   16         public override double Cost()
   17         {
   18             return 0.25 + coffe.Cost();
   19         }
   20
   21         public override string GetDescription()
   22         {
   23             return coffe.GetDescription() + ", Mokka";
   24         }
   25     }

    8     public class Suggar : CondimentsDecorator
    9     {
   10         
   11
   12         public Suggar(Coffe coffe)
   13         {
   14             this.coffe = coffe;
   15         }
   16
   17         public override double Cost()
   18         {
   19             return 0.15 + coffe.Cost();
   20         }
   21
   22         public override string GetDescription()
   23         {
   24             return coffe.GetDescription() + ", suggar";
   25         }
   26     }



    8     class Program
    9     {
   10         static void Main(string[] args)
   11         {
   12             Coffe coffe = new Jawajska();
   13             Console.WriteLine(coffe.GetDescription());
   14             Console.WriteLine(coffe.Cost());
   15             coffe = new Milk(coffe);
   16             coffe = new Suggar(coffe);
   17             coffe = new Mokka(coffe);
   18             Console.WriteLine(coffe.GetDescription());
   19             Console.WriteLine(coffe.Cost());
   20         }
   21     }


Wynikiem działania powyższego kodu będzie:

Na koniec jeszcze małe podsumowanie na temat użycia tego wzorca.
Przydaje się on wszędzie gdzie mamy potrzebę "przyozdabiania". Przydać się może np. w aplikacji która pracuje na plikach graficznych, edycji wideo itp.

Wzorzec ten jest szczególnie wykorzystywany w przypadku biblioteki I/O.