V minulém díle jsme si ukazovali, jak se ve VB.NET vykreslují grafy pomocí GDI+. Šlo by na to jistě použít nějakou již hotovou komponentu, která je k tomu určena, ale chtěl jsem předvést praktické využití rozhraní GDI+, potažmo jmenného prostoru System.Drawing.
Dnes se vrhneme na trochu jinou oblast programování, a to pokročilejší práci se soubory. Do souboru totiž občas potřebujeme uložit číslo, datum, případně nějaká binární data, a tak si ukážeme, jak to udělat nejsnadněji. Protože již nejsme začátečníci, ale mírně pokročilí, a víme již něco o objektech, ukážeme si, jak vytvořit objekt vlastní a jak jej používat. Tato technika se nazývá objektově orientované programování a zvláště v .NET frameworku ji potkáme na každém kroku.
Začínáme
V tomto díle si jako ukázku napíšeme velmi jednoduchou aplikaci, která bude pracovat s informacemi o osobách a bude je umět ukládat a načítat ze souborů. Vytvořte si tedy nový projekt a na formulář přidejte 4 komponenty Label, dále textová pole txbJmeno a txbPrijmeni, pak komponentu DateTimePicker s názvem dtpNarozeni a nakonec komponentu NumericUpDown s názvem numPocetDeti. Nakonec přidejte jeden PictureBox pro vložení fotografie osoby a tři tlačítka – Nový, Otevřít a Uložit. Celé by to mohlo vypadat například takto:
Komponenta DateTimePicker umožňuje interaktivně zadávat datum a případně i čas. Můžete ji rozbalit a hodnotu vybrat z kalendáříku, nebo ji jednoduše zapíšete do pole klávesnicí, pokud datum znáte z hlavy. Komponenta NumericUpDown umožňuje zadávat čísla (desetinná i celá) a kontroluje, jestli tam uživatel nezadává nesmysly.
Data se budou ukládat do textového souboru. Protože máme různé typy dat, budeme je chtít uložit v nějakém speciálním formátu. Jednotlivé položky (jméno, příjmení, datum narození, počet dětí a fotografii) budeme oddělovat středníky a domluvíme se, že texty budeme dávat do uvozovek. Problém bude s fotografií, protože obrázek je vlastně smečka bajtů, které samozřejmě mohou obsahovat i středník, čímž by nám mohl nastat problém. Pokud chceme ukládat binární data do textových souborů, obecně se používá tzv. base64 kódování. Celá posloupnost bajtů se převede speciálním způsobem na trochu delší posloupnost čísel, písmen a symbolů +, / a =, tedy na posloupnost znaků, které nebudou dělat v textovém souboru problémy. Přesný způsob zápisu je nad rámec tohoto článku, pokud vás přesto zajímají, můžete se podívat na wikipedii. Výhoda je, že se jedná o standardizovaný způsob a .NET framework pro něj má připravené funkce Convert.FromBase64String a Convert.ToBase64String, které provedou samotné zakódování a následné dekódování.
Stručný úvod do tříd a objektů
Velmi často se nám stává, že potřebujeme nějakou část aplikace napsat tak, abychom ji mohli použít vícekrát, a to nezávisle na sobě. Přestože se dá vždy obejít bez objektů, je velmi často (ale ne vždy) vhodné napsat si vlastní třídu. Třída je vlastně šablona pro objekt, je to sada vlastností, metod (jiný název pro procedury) a proměnných, se kterou můžeme pracovat. Nejlepší bude ukázat si to v praxi. Napíšeme si nejprve třídu RecordWriter, která bude zapisovat hodnoty různých datových typů do souboru. V podokně Solution Explorer klikněte pravým tlačítkem na název projektu a vybereme položku Add / Class. V dialogovém okně napíšeme název RecordWriter.vb a po kliknutí na tlačítko OK se nám vytvoří nová třída RecordWriter.
Do tohoto souboru vložtě tento kód:
Dim w As IO.StreamWriter
Sub New(ByVal filename As String)
w = New IO.StreamWriter(filename)
End Sub
Public Sub Write(ByVal i As Integer)
w.Write(i)
w.Write(";")
End Sub
Co jsme právě udělali? Nejprve jsme do naší třídy RecordWriter nadeklarovali proměnnou w typu IO.StreamWriter. Tento datový typ známe, slouží k zápisu dat do souboru.
Dále následuje procedura s názvem New, která potřebuje jeden parametr filename. Uvnitř vytvoříme nový objekt StreamWriter a přiřadíme mu cestu, kterou jsme dostali v parametru filename. Tento objekt přiřadíme do proměnné w.
Je nutné říci, že procedura New není jen tak obyčejná procedura. Říká se jí konstruktor a spustí se vždycky při vytvoření objektu dané třídy. Náš konstruktor má jeden povinný parametr filename, to znamená, že když někde v projektu budeme chtít vytvořit objekt RecordWriter, musíme napsat Dim x As New RecordWriter(“C:\soubor.txt“), zkrátka musíme předat objektu hodnotu parametru filename. Pokud to provedeme, do proměnné w tohoto objektu se vytvoří nový objekt StreamWriter, protože jsme to to napsali do konstruktoru. Pokud tedy nějakou metodu uvnitř třídy pojmenujeme New, spustí se při vytváření objektu pomocí klíčového slova New. Pokud konstruktor potřebuje nějaké parametry, musíme je při vytváření objektu předat.
Obecně platí, že do konstruktoru patří kód, který třídu připraví a nachystá tak, abychom pak mohli volat její další metody.
A nakonec jsme do třídy přidali metodu Write, která dostane číslo i a zapíše jej do našeho StreamWriteru. Hned za něj napíše středník. Abychom mohli tuto metodu zavolat, musíme mít připraven objekt StreamWriter. Proto jsme jeho vytvoření dali do konstruktoru, abychom se o něj později nemuseli starat a měli jej připravený.
Pokud nyní budeme chtít naši třídu použít a zapsat do souboru 3 čísla, může to vypadat třeba takto:
Dim x As New RecordWriter("C:\data.txt")
x.Write(16)
x.Write(8)
x.Write(-3)
Možná si řeknete, proč tady blbneme s objekty, když jsme to mohli napsat rovnou. Ale díky tomu, že jsme napsali třídu, můžeme klidně vytvořit deset objektů RecordWriter, kde každý bude mít na starosti úplně jiný soubor a pokud na něm zavoláme Write, zapíše právě do toho svého souboru a nikam jinam. Každý objekt RecordWriter se stará o svůj stream a zapisuje jen do něj, každý má totiž svou vlastní proměnnou w. Libovolné dva objekty RecordWriter jsou na sobě nezávislé, mají však stejnou strukturu. Vlastně to již známe z komponent na formuláři – můžeme přidat několik tlačítek, všechny mají stejné vlastnosti, ale každé může mít v dané vlastnosti jinou hodnotu, každé tlačítko má své události, které se spouští jednotlivě.
Třída nebo objekt?
Je třeba rozlišovat rozdíl mezi třídou a objektem. Třída je předpis pro to, jak má objekt vypadat. Obsahuje deklarace proměnných, procedur atd. Používá se také jako datový typ při deklaraci proměnné, která má uchovávat daný objekt. Objekt je jedna konkrétní instance (exemplář) dané třídy. Stručně řečeno, pokud se podíváte na předchozí kód, tak RecordWriter je třída a proměnná x obsahuje objekt. Od každé třídy můžeme vytvořit mnoho objektů, třída je ale vždy jen jedna. Třída je zkrátka jen jakýsi vzor.
Dokončení třídy RecordWriter
Naši třídu RecordWriter ještě nemáme hotovou. Rozhodně tam musíme přidat metodu Close, kterou musíme zavolat, aby se nám soubor zavřel. Pokud soubor nezavřeme, nebude přístupný ostatním programům, což nechceme, a navíc se mohou nějaká data ztratit, protože zapisování na disk je pomalé a operační systém data zapíše, až když je jich víc pohromadě, anebo pokud soubor zavíráme. Pokud jej nezavřeme, několik posledních bajtů se může ztratit.
Pokud do třídy doplníme ještě pár dalších metod, bude vypadat takto:
Public Class RecordWriter
Dim w As IO.StreamWriter
Sub New(ByVal filename As String)
'otevřít soubor pro zápis
w = New IO.StreamWriter(filename, False, System.Text.Encoding.UTF8)
End Sub
Public Sub Write(ByVal number As Integer)
'zapsat číslo
w.Write(number)
w.Write(";")
End Sub
Public Sub Write(ByVal text As String)
'zapsat řetězec (vyházet z něj všechny uvozovky)
w.Write(Chr(34) & text.Replace(Chr(34), "") & Chr(34))
w.Write(";")
End Sub
Public Sub Write(ByVal bytes() As Byte)
'zapsat bajty (base64 zakódované)
w.Write(Convert.ToBase64String(bytes))
w.Write(";")
End Sub
Public Sub Write(ByVal dateTime As Date)
'zapsat datum
w.Write(dateTime.ToString("r"))
w.Write(";")
End Sub
Public Sub Close()
'zavřít soubor
w.Close()
End Sub
End Class
Všimněte si, že jsem záměrně vytvořil několik metod s názvem Write, které se liší datovým typem parametru. Tomuto způsobu se říká přetěžování metod a pokud metodu voláme, kompilátor VB.NET podle datového typu parametru sám určí, která metoda se zavolá. výhodou je hlavně zpřehlednění kódu – ať zapisujeme do souboru text, číslo nebo datum, používáme jednu metodu. Přetěžování je v .NET frameworku velmi časté a bez něj bychom měli daleko více metod, které se od sebe liší jen trošku, ale chtějí maličko jiné parametry.
Dále si všimněte, jak zapisujeme do souboru datum. Používáme dateTime.ToString(“r“). Písmeno r označuje formát, v jakém se má datum uložit, v tomto případě je to standardizovaný formát pro výměnu data popsaný v RFC specifikacích.
Co dál?
Tímto jsme tedy vytvořili třídu RecordWriter, která nám snadno a rychle umožňuje zapisovat do souboru kombinace hodnot různých datových typů, které odděluje středníkem. Můžeme ji snadno použít a na pár řádků uložit údaje o osobě. Jak již možná tušíte, napíšeme si také třídu RecordReader, která bude data ze souboru v tomto formátu číst.
Čtení dat ze souboru
Vytvořte si další třídu RecordReader a přidejte do ní tento kód.
Dim r As IO.StreamReader
Sub New(ByVal filename As String)
'otevřít soubor pro čtení
r = New IO.StreamReader(filename, System.Text.Encoding.UTF8)
End Sub
Public Function ReadInt() As Integer
'přečíst číslo
Return CInt(Me.Read())
End Function
Public Function ReadString() As String
'přečíst řetězec a odstranit uvozovky
Return Me.Read().Replace(Chr(34), "")
End Function
Public Function ReadBytes() As Byte()
'přečíst pole bajtů
Return Convert.FromBase64String(Me.Read())
End Function
Public Function ReadDate() As Date
'přečíst datum
Return Date.Parse(Me.Read())
End Function
Public Sub Close()
'zavřít soubor
r.Close()
End Sub
VB.NET vám ohlásí chyby, protože jsem do něj úmyslně nezahrnul jednu proceduru. Zatím zde tedy máme konstruktor, proměnnou r typu StreamReader, která bude přistupovat k souboru, ze kterého čteme, a pak metodu Close, která soubor zavře. Pro čtení různých datových typů máme metody ReadInt, ReadString, ReadDate a ReadBytes. Zde již nemůžeme použít přetěžování, protože metody se liší jen datovým typem návratové hodnoty, nikoliv parametry. Přetěžování podle návratových hodnot kompilátor neumí, musely by se totiž řešit určité sporné situace a nepřineslo by to tolik užitku.
Private Function Read() As String
'přečíst ze souboru jeden záznam a vrátit jej jako String
Dim sb As New System.Text.StringBuilder()
Dim ch(0) As Char 'pole, do kterého budeme číst znaky ze souboru
r.ReadBlock(ch, 0, 1) 'načíst první znak
While ch(0) <> ";"c 'opakujeme, dokud nenarazíme na středník
sb.Append(ch(0), 1) 'přidat ho do stringu
r.ReadBlock(ch, 0, 1) 'přečíst další znak
End While
Return sb.ToString()
End Function
Všechny 4 metody volají tuto metodu Read. Ta nám přečte text ze souboru až do následujícího středníku, vrátí nám vlastně další záznam. Každá metoda si jej pak převede na příslušný datový typ a vrátí jej. Všimněte si také, že metody ReadString, ReadInt atd. jsou deklarovány jako Public, kdežto naše metoda Read je deklarována jako Private. Rozdíl je v tom, že Public (veřejné) metody můžeme volat zvenku (tedy třeba z proměnné x, která obsahuje instanci objektu), zatímco Private (soukromé) metody mohou být zavolány pouze z dané třídy. Nechceme, aby nám někdo volal Read, tato metoda slouží jen pro potřeby naší třídy RecordReader.
A samotné čtení probíhá po znacích. Protože StreamReader má jen metody pro čtení pole znaků, uděláme si pole ch o velikosti 1 znak (je třeba rozlišovat mezi typy Char a Byte, používáme kódování Unicode, kde 1 znak nemusí být 1 bajt!). Pak jej předáme metodě ReadBlock, která čte po znacích. A pokud znak není středník tak jej přidáme do StringBuilderu a přečteme znak další. Opakujeme, dokud nenarazíme na středník. Nakonec tato metoda vrátí hotový záznam a skončí.
Tímto způsobem jsme si napsali třídu, se kterou nám půjde práce se souborem jedna radost. Navíc, protože jsme kód oddělili do samostatných tříd, můžeme tyto třídy použít i jinde a nemusíme neustále psát stejný kód znovu.
Dokončení celé aplikace
Tlačítko Nový bude na inplementaci triviální - stačí vynulovat všechna pole.
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
'ujistit se, že to uživatel chce
If MsgBox("Neuložené změny aktuální osoby budou ztraceny! Pokračovat?", MsgBoxStyle.Question Or MsgBoxStyle.YesNo) = MsgBoxResult.No Then Exit Sub
'vynulovat hodnoty ve formuláři
txbJmeno.Text = ""
txbPrijmeni.Text = ""
dtpNarozeni.Value = Now
numPocetDeti.Value = 0
picFotka.Image = Nothing
End Sub
Jediná zajímavá věc, která nás může potkat, je druhý parametr u funkce MsgBox. Říkáme, že chceme parametr Question a YesNo (aby se použili oba, používáme mezi ně operátor Or, který se v tomto případě používá, přestože znamená nebo. V tomto případě má funkci bitového součtu. Pokud funkce MsgBox vrátí hodnotu No, skončíme proceduru předčasně.
Pokud dvakrát klikneme na PictureBox, měl by se zobrazit dialog pro výběr obrázku. Přidejte tedy na formulář tři dialogy - jeden SaveFileDialog a dva OpenFileDialogy. Pak do procedury DoubleClick komponenty PictureBox1 vložte tento kód:
Private Sub picFotka_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles picFotka.Click
'vybrat obrázek
If OpenFileDialog2.ShowDialog() = Windows.Forms.DialogResult.OK Then
picFotka.Image = Image.FromFile(OpenFileDialog2.FileName)
End If
End Sub
Je vhodné nastavit hodnotu vlastnosti Filter komponentě OpenFileDialog2 na hodnotu Všechny obrázky|*.jpg;*.bmp;*.gif;*.png.
Načtení dat dpak proběhne následovně:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
'ujistit se, že to uživatel chce
If MsgBox("Neuložené změny aktuální osoby budou ztraceny! Pokračovat?", MsgBoxStyle.Question Or MsgBoxStyle.YesNo) = MsgBoxResult.No Then Exit Sub
'zeptat se na soubor
If OpenFileDialog1.ShowDialog() = Windows.Forms.DialogResult.OK Then
'pokud uživatel vybral soubor, načíst jej
Dim rr As New RecordReader(OpenFileDialog1.FileName)
'přečíst data
txbJmeno.Text = rr.ReadString()
txbPrijmeni.Text = rr.ReadString()
dtpNarozeni.Value = rr.ReadDate()
numPocetDeti.Value = rr.ReadInt()
'načíst obrázek
Dim bytes() As Byte = rr.ReadBytes()
If bytes.Length = 1 Then
'prázdný obrázek
picFotka.Image = Nothing
Else
'načíst obrázek
Dim ms As New IO.MemoryStream()
ms.Write(bytes, 0, bytes.Length)
ms.Position = 0
picFotka.Image = Image.FromStream(ms)
ms.Close()
End If
'zavřít soubor
rr.Close()
End If
End Sub
Nejprve se zeptáme, jestli uživatel opravdu chce to, co chce, pokud ne, tak skončíme rovnou. Pak zobrazíme dialog pro otevření souboru a pokud uživatel operaci nezruší, vytvoříme si nový objekt RecordReader. Jako parametr do konstruktoru předáme cestu k vybranému souboru. Pak jen voláme metody ReadString, ReadDate, ReadInt a ReadBytes, přičemž to, co vrátí, přiřadíme do příslušných komponent. Načtení obrázku je o něco zajímavější - mohli bychom obrázek uložit na disk a pak jej otevřít z disku přes Image.FromFile, což umíme. Ale není to jediná cesta. Můžeme vytvořit tzv. MemoryStream, což je jakýsi proud dat, do kterého můžeme zapisovat a číst. Do pole bytes si tedy načteme bajty ze souboru, pokud je velikost pole 1, znamená to, že žádný obrázek k osobě nemáme (ten 1 bajt je dán jen a jen tím, jak budeme soubor s osobou ukládat; pokud obrázek u osoby není, uložíme jeden jediný bajt s hodnotou 0), tak jen zrušíme obrázek v PictureBoxu, pokud tam nějaký je. Pokud ale délka pole je jiná, pak vytvoříme objekt MemoryStream a metodou Write do něj "nalijeme" naše bajty. První parametr je naše pole bajtů, druhý parametr je index v poli, kde se má začít (my jedeme od začátku, takže dáme 0) a poslední parametr je počet bajtů, které se mají zapsat, což zjistíme přes bytes.Length, které vrací počet prvků v poli. Jakmile data do streamu zapíšeme, musíme nastavit pozici na začátek, protože z tohoto streamu budeme číst obrázek od začátku, nastavíme tedy Position na nulu. Pak jen do PictureBoxu přiřadíme nový obrázek, a to zavoláním Image.FromStream. Nakonec stream zavřeme, je to stejné, jako se soubory. Nezapomeneme také zavolat metodu Close na našem objektu, který čte ze souboru, aby se daný soubor zavřel a nezůstal uzamknut.
Uložení dat bude vypadat takto:
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
'zeptat se na soubor
If SaveFileDialog1.ShowDialog = Windows.Forms.DialogResult.OK Then
'pokud uživatel vybral soubor, otevřít jej a uložit
Dim rw As New RecordWriter(SaveFileDialog1.FileName)
'uložit data
rw.Write(txbJmeno.Text)
rw.Write(txbPrijmeni.Text)
rw.Write(dtpNarozeni.Value)
rw.Write(CInt(numPocetDeti.Value))
If picFotka.Image IsNot Nothing Then
'uložit obrázek do MemoryStreamu
Dim ms As New IO.MemoryStream()
picFotka.Image.Save(ms, Imaging.ImageFormat.Jpeg)
rw.Write(ms.ToArray())
ms.Close()
Else
'uložit pole s jedním nulovým bajtem
rw.Write(New Byte() {0})
End If
'zavřít soubor
rw.Close()
End If
End Sub
Zeptáme se na soubor, pokud jej uživatel vybere, vytvoříme náš objekt RecordWriter a zapíšeme jméno, příjmení, datum narození a počet dětí. Pokud uživatel vybral obrázek (IsNot Nothing znamená pokud není prázdný, analogicky Is Nothing znamená je prázdný), vytvoříme MemoryStream a obrázek metodou Save uložíme do tohoto streamu ve formátu Jpeg. Pokud na stream zavoláme metodu ToArray, vrátí nám pole bajtů celého streamu. Ty předáme jako parametr metodě Write našeho objektu, který tyto bajty sám zakóduje a zapíše do souboru. Nakonec zavřeme stream a i soubor, do kterého ukládáme. Pokud uživatel obrázek nevybral, do souboru se zapíše pole jednoho bajtu, který má hodnotu 0. Naše objekty totiž nejsou připraveny na prázdné hodnoty, něco tam tedy uložit musíme.
Závěrem
Pokud vám přijdou objekty moc složité, nezoufejte. Spousty věci se dají programovat bez nich, což si ukážeme v příštím díle, kde si procvičíte logické myšlení. Pravděpodobně jsem základy objektově orientovaného programování nevysvětlil formálně a teoreticky nejlépe, věřím ale, že vás tento článek alespoň naučí použít je v praxi. Pokud nevidíte výhodu objektového přístupu, tak v tomto konkrétním případě může spočívat třeba v tom, že načítání a ukládání dat, tedy samotná podstata aplikace, je poměrně elegantně odděleno od zapisování a čtení ze souboru. Procedury ukládání a načítání jsou díky tomu přehlednější, prostě jen říkáte, co se má zapsat nebo co se má přečíst. Samotné ukládání a čtení se řeší jinde, dvě různé části aplikace nejsou tedy smíchány do sebe. Jakékoliv dotazy a náměty pište do diskuse.