Vykreslujeme graf

14. díl - Vykreslujeme graf

Tomáš Herceg       31.08.2007       VB.NET, Algoritmy, WinForms, Grafika       21702 zobrazení

V tomto díle seriálu si zopakujeme načítání textových souborů a podíváme se na některé další funkce ze jmenného prostoru System.Drawing. Napíšeme aplikaci, která načte data ze souboru a zobrazí z nich sloupcový graf.

V minulém díle tohoto seriálu jsme si ukázali základy vykreslování v rozhraní GDI+ pomocí funkcí ze jmenného prostoru System.Drawing. Dnes v tom budeme pokračovat a zopakujeme některé věci z minulých dílů (načítání souboru). Podle dat v souboru vykreslíme sloupcový graf. Samozřejmě pro vykreslování grafů existuje mnoho jiných způsobů a komponent, ale nás bude zajímat především vykreslování. Máme toho před sebou poměrně dost, takže nezbývá, než začít.

Začínáme

Vytvořte si nový projekt, formuláři nastavte vlastnost FormBorderStyle na hodnotu FixedDialog, vlastnost MaximizeBox na hodnotu False a vlastnost Size na hodnotu 820;650. Tím jsme nastavili velikost formuláře a zakázali jeho roztahování. Do projektu nyní přidáme datový soubor - pravým tlačítkem klikněte na název projektu v podokně Solution Explorer a vyberte z nabídky možnost Add / New Item.... V dialogovém okně pro výběr typu nové položky zvolte typ Text File a pojmenujte jej graf.txt. V okně vlastností mu nastavte hodnotu vlastnosti Copy To Output Directory na CopyAlways. Tím zajistíme, že před spuštěním Visual Studio zkopíruje tento soubor do stejné složky, jako je spustitelný EXE soubor. Bez toho by program soubor nenašel a vyhodil by chybu.

Soubor graf.txt nyní otevřete a vložte do něj tato data:

12
leden
120000
únor
110000
březen
93500
duben
135600
květen
148000
červen
163400
červenec
170000
srpen
172000
září
183200
říjen
193050
listopad
205600
prosinec
235000

První řádek obsahuje celkový počet záznamů v souboru, v našem případě 12. Na dalších řádcích jsou již samotné záznamy. Každý záznam je tvořen názvem období a ziskem v Kč za dané období (každá z informací je na samostatném řádku). V lednu byl tedy zisk 120 000 Kč, v únoru pak 110 000 Kč atd. Údaje jsou samozřejmě vymyšlené.

Odbočka - Struktury

Pokud jste zkoušeli něco programovat sami, možná vás napadlo, že nám obyčejné datové typy (String, Integer atd.) nestačí. Bylo by jistě pěkné a elegantní, kdybychom v každém prvku pole mohli uchovávat více hodnot s různými datovými typy. Mohli bychom pak míti pole Osoby, které by pro každou osobu obsahovalo její jméno(String), příjmení (String) a věk (stačí Byte - hodnoty 0 až 255). Přesně tohle je možné provést pomocí deklarace vlastní struktury.

Strukturu je speciální datový typ složený z více jednodušších datových typů. Aby kompilátor věděl, jaké údaje chceme ve struktuře uchovávat, musíme nejprve strukturu nadeklarovat. Deklarace nesmí být uvnitř procedury ani funkce, vypadá třeba takto (nekopírujte do projektu!):

    Structure Osoba
        Dim Jmeno, Prijmeni As String
        Dim Vek As Byte
    End Structure

Její použití pak může vypadat třeba takto (název struktury používáme jako datový typ, k jednotlivým proměnným přistupujeme přes tečku):

        Dim os As Osoba
        os.Jmeno = "Tomáš"
        os.Prijmeni = "Herceg"
        os.Vek = 19

Do struktur je možno přidat i deklarace procedur a funkcí, ale to zatím využívat nebudeme.

 Načtení datového souboru

 Jak asi tušíte, odbočky nejsou v článku pro srandu tvorům s dlouhýma ušima, ale k doplnění teorie, která by se nám mohla hodit. Pokud se podíváme na soubor s daty, uvidíme tam záznamy skládající se ze dvou položek. A právě na ně si vytvoříme strukturu. Tento kód tedy zkopírujte do projektu (hned za řádek Public Class Form1, ne do procedury):

    'struktura pro záznam v souboru
    Structure Zaznam
        Dim Text As String
        Dim hodnota As Integer
    End Structure

    'pole záznamů v grafu
    Dim zaznamy() As Zaznam

 Vytvořili jsme si tedy pole, v jehož každém prvku uchováváme zisk za období a název tohoto období. Pozastavíme se ještě nad deklarací pole - datový typ je název struktury. Ale nezadali jsme velikost pole, protože ji neznáme. Dali jsme tedy jen (), aby se poznalo, že to je pole. Velikost určíme až v okamžiku, kdy ji budeme znát (až budeme mít načtený soubor).

Dále ještě podotýkám, že pokud proměnnou nadeklarujete mimo proceduru, můžeme tuto proměnnou používat ve všech procedurách v dané třídě (formuláři). Pokud proměnnou nadeklarujete uvnitř procedury, funguje tato proměnná pouze v dané proceduře a po skončení procedury se její hodnota ztratí!

Načítat data ze souboru již umíme, vytvoříme nový objekt StreamReader, předáme mu název souboru a volitelně ještě kódování (pokud soubor vytvoříte ve Visual Studiu a neměnili jste kódování v nastavení nebo při ukládání souboru, mělo by být použito kódování Windows-1250, čili System.Text.Encoding.Default, pokud nefunguje, zkuste System.Text.Encoding.UTF8). Celé načtení souboru tedy provede tento kód v proceduře Form1_Load, který si přidejte do projektu:

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'otevřít soubor pro čtení
        Dim sr As New IO.StreamReader("graf.txt", System.Text.Encoding.Default)

        'načíst počet záznamů v souboru
        Dim pocet As Integer = CInt(sr.ReadLine())

        'nadimenzovat pole se záznamy
        ReDim zaznamy(pocet - 1)

        'projít v cyklu záznamy a načítat hodnoty ze souboru do pole
        For i As Integer = 0 To pocet - 1
            zaznamy(i).Text = sr.ReadLine()     'načíst popis
            zaznamy(i).hodnota = CInt(sr.ReadLine())   'načíst částku
        Next

        'zavřít soubor
        sr.Close()
    End Sub

Nejprve tedy otevřeme soubor pro čtení, pak zjistíme počet záznamů, klíčovým slovem ReDim nastavíme dodatečně velikost pole, protože již ji známe, a pak v cyklu načítáme nejprve popisek a pak částku v daném období. Úplně nakonec datový soubor zavřeme. Máme tedy pole zaznamy s načtenými hodnotami, nezbývá, než vykreslit graf.

Co bude třeba, abychom graf vykreslili?

Protože chceme, aby nám graf optimálně vyplnil plochu formuláře, je dobré znát rozestupy sloupců a nejvyšší sloupec, podle kterého graf roztáhneme na výšku.

Pokud bychom formulář svisle rozřezali na 5 stejných dílů, získáme 4 svislé čáry. A právě na těchto čarách je nejlepší vykreslit sloupce. Rozestup mezi čarami je pak šířka formuláře vydělená počtem dílů, na kolik bychom formulář rozřezali, čili o jednu více, než je sloupců. Pokud je tedy sloupců 12, rozestup je šířka formuláře / 13. To bude vzdálenost mezi středy spodních stran sloupců.

O poznání zajímavější je problém druhý, a to nalezení nejvyššího sloupce. Je to trochu k zamyšlení - jak najít nejvyšší hodnotu v poli hodnot? Provedeme drobný podvod - prohlásíme, že nejvyšší je první sloupec (uložíme si jeho výšku do proměnné max, kde chceme mít výšku toho nejvyššího sloupce). Pak v cyklu projdeme sloupce ostatní a pokud náhodou najdeme nějaký vyšší (vyšší než hodnota max), prohlásíme za nejvyšší tento nový (jeho výšku uložíme do max). Jakmile projdeme všechny sloupce, budeme mít v proměnné max výšku toho nejvyššího sloupce.

A k čemu že nám zjištění hodnoty nejvyššího sloupce bude? Pokud má okno výšku 600 pixelů, řekneme, že nejvyyší sloupec vykreslíme 500 pixelů vysoký. Pokud si pamatujete ještě ze školy trojčlenku, můžeme takto spočítat výšky všech ostatních sloupců:

max (= 100000) ....................... 500 pixelů
                     67000 ....................... x pixelů            

Jelikož se jedná o přímou úměrnost, výšku sloupce s částkou 67 000 Kč v pixelech spočítáme jako x = 67000 / max * 500 pixelů.

Pokud známe rozestup sloupců, jejich pozice bude pak (i + 1) * rozestup, kde i je číslo sloupce (od nuly). Pokud je rozestup třeba 100 pixelů, první sloupec pak bude mít X souřadnici 100, druhý 200 atd. Pokud chceme šířku sloupce třeba 30 pixelů, od spočítané X souřadnice odečteme 15 (protože souřadnice je střed sloupce) a vykreslíme jej (vykreslení totiž nechce střed, ale jeden z rohů).

    Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
        e.Graphics.Clear(Color.White)

        'zjistit hodnotu nejvyššího sloupce
        Dim max As Integer = zaznamy(1).hodnota
        For i As Integer = 1 To zaznamy.Length - 1
            If zaznamy(i).hodnota > max Then max = zaznamy(i).hodnota
        Next

        'vykreslit graf
        Dim rozestup As Integer = 800 / (zaznamy.Length + 1)
        Dim vyska As Integer = 500
        For i As Integer = 0 To zaznamy.Length - 1
            Dim x As Integer = (i + 1) * rozestup - 15
            Dim y As Integer = zaznamy(i).hodnota / max * vyska
            e.Graphics.FillRectangle(Brushes.Blue, x, 0, 30, y)
        Next
    End Sub

Nejprve jsme tedy zjistili částku v nejvyšším sloupci, pak spočítali rozestup (šířka okna děleno počet sloupců zvýšený o 1) a výšku nejvyššího sloupce nastavili na 500. V cyklu pak počítáme proměnnou x (pozici X levého horního rohu sloupce pomocí rozestupů) a proměnnou y (výška sloupce v pixelech). Nakonec sloupce vykreslíme - pozici x známe, pozici y levého horního rohu dáme zatím nula, šířku sloupce 30 a výšku spočítané y.

Tento výsledek není jistě optimální a jistě se s ním nespokojíme, ale základ tam je. Máme již zachovány poměry výšek sloupců, největším problémem je asi to, že graf je otočený vzhůru nohama. V grafech totiž souřadnice Y roste od osy Y nahoru, kdežto na monitoru roste od osy Y dolů.

Graf vzhůru nohama

Určitě by to chtělo graf otočit, řekneme tedy, že spodní strana grafu bude začínat na Y souřadnici 580. Horní levý roh tedy bude o y pixelů výše, kde y je výška sloupce. Změňte tedy příslušný řádek, který vykresluje sloupce, takto:

e.Graphics.FillRectangle(Brushes.Blue, x, 580 - y, 30, y)

Graf bez popisků

Tento graf je již o mnoho lepší, zbývá doplnit popisky měsíců a asi by bylo dobré vypsat nad sloupce jednotlivé částky.

Textové popisky pod sloupci

Ještě jsme si vůbec neukazovali, jak vykreslovat text. Není to nic složitého, ale je tam jeden zádrhel. Objekt Graphics má metodu DrawString, která ale požaduje souřadnice levého horního rohu daného textu, což se nám moc nehodí, protože text potřebujeme vycentrovat na střed sloupce. Existuje ale metoda MeasureString, která nám vrátí rozměry předaného textu. Obě dvě metody vyžadují jako parametr předat font, který se má použít, nejjednodušší je předat Me.Font, čili hodnotu vlastnosti Font aktuálního formuláře.

Díky metodě MeasureString můžeme vytvořit vlastní proceduru DrawText, které předáme souřadnice středu textu, procedura si podle velikosti tohoto textu spočítá jeho rozměry (stačí od souřadnic středu odečíst polovinu šířky resp. výšky textu) a dopočtá souřadnice levého horního rohu. Pak text vykreslí:

    Sub DrawText(ByVal g As Graphics, ByVal txt As String, ByVal x As Integer, ByVal y As Integer)
        'vykreslit text zadaný středem
        Dim velikost As SizeF = g.MeasureString(txt, Me.Font)
        Dim s As Integer = velikost.Width / 2
        Dim v As Integer = velikost.Height / 2
        g.DrawString(txt, Me.Font, Brushes.Black, x - s, y - v)
    End Sub

Podotýkám, že v proceduře DrawText používáme proměnnou g, což je objekt Graphics, na který vykreslujeme. Je to jeden z parametrů této procedury, při volání do něj předáváme hodnotu e.Graphics. Ta je totiž dostupná pouze v události Paint, nikde jinde již ne. Proceduře DrawText předáme souřadnice středu vypisovaného textu, metodě DrawString je nutné předat souřadnice levého horního rohu vypisovaného textu.

Pokud tedy přidáme do procedury pro vykreslování volání této funkce pro kreslení textu, můžeme snadno pod sloupce vykreslit názvy měsíců.

    Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
        e.Graphics.Clear(Color.White)

        'zjistit hodnotu nejvyyššího sloupce
        Dim max As Integer = zaznamy(1).hodnota
        For i As Integer = 1 To zaznamy.Length - 1
            If zaznamy(i).hodnota > max Then max = zaznamy(i).hodnota
        Next

        'vykreslit graf
        Dim rozestup As Integer = 800 / (zaznamy.Length + 1)
        Dim vyska As Integer = 500
        For i As Integer = 0 To zaznamy.Length - 1
            Dim x As Integer = (i + 1) * rozestup
            Dim y As Integer = zaznamy(i).hodnota / max * vyska

            'vykreslit sloupec
            e.Graphics.FillRectangle(Brushes.Blue, x - 15, 580 - y, 30, y)

            'vykreslit popisky pod sloupce
            DrawText(e.Graphics, zaznamy(i).Text, x, 590)
        Next
    End Sub

Graf s popisky

Transformace

Rozhraní GDI+ podporuje tzv. transformace. Před tím, než něco vykreslím, můžu nastavit kombinaci transformací, které nějakým způsobem pozmění vykreslovaný element. V praxi existuje několik jednoduchých transformací - posunutí, otočení a změna měřítka. Jejich použití je poměrně jednoduché.

Náš graf by jistě potřeboval nad každý sloupec umístit danou částku, pokud ji však vykreslíme normálně, budou se částky překrývat (jsou moc široké). Ideální by bylo, kdybychom částky pootočili, nebudou se totiž překrývat.

Potřebujeme tedy rotaci (otočení). Nejjednodušší rotace je rotace kolem počátku, tedy kolem bodu [0, 0]. My bychom potřebovali ale rotovat kolem jiného bodu, což však můžeme krásně vyřešit tak, že text vykreslíme do počátku, pak jej otočíme, a nakonec posuneme tam, kde má správně být. Transformace se totiž dají kombinovat.

Abych to neprotahoval, zde je přetížení procedury DrawText, které podporuje i rotaci. Přidejte jej jako další proceduru do projektu (tu starou nepřepisujte, jedná se o přetížení):

    Sub DrawText(ByVal g As Graphics, ByVal txt As String, ByVal x As Integer, ByVal y As Integer, ByVal rotation As Integer)
        'vykreslit text zadaný středem a rotací
        g.TranslateTransform(x, y)
        g.RotateTransform(rotation)

        'nakreslit text do počátku
        DrawText(g, txt, 0, 0)

        'zrušit transformaci
        g.ResetTransform()
    End Sub

TranslateTransform je posunutí, zadáváme o kolik bodů chceme posunovat ve směru osy X a Y. RotateTransform je rotace, zadáváme úhel ve stupních. Nejprve se vždy nastavují transformace, pak se vykresluje. Nová transformace nezruší tu starou! Po vykreslení je vhodné transformaci zrušit zavoláním ResetTransform, aby se neaplikovala na další vykreslované prvky.

Vykreslení částek nad sloupce a vylepšení celkového dojmu

Pokud tedy dovnitř cyklu přidáme ještě vykreslení otočeného textu o -60 stupňů, vypíší se nad sloupce požadované částky. Ještě můžeme pro sloupce vytvořit štětec s přechodem barev ze světlemodré (nahoře v grafu) na modrou (dole v grafu). Dále jsem ještě založil proměnnou sirka, která podle rozestupu určí polovinu šířky sloupce (aby nebyla šířka sloupce určena pevně, pokud bychom zvětšili počet záznamů, vypadalo by to divně).

Ještě poslední věc - funkce String.Format slouží k dosazování do textu. První parametr je šablona a další parametry jsou dosazované hodnoty (může jich být více). V šabloně se všechny výskyty {0} nahradí první hodnotou,  {1} zase druhou hodnotou atd. Takže String.Format("Číslo {0} a {1}.", 15, 32) vrátí Číslo 15 a 32. Pokud za číslo ve složené závorce dáme dvojtečku, můžeme určit formát. Je mnoho parametrů pro určení formátu, nás zajímá hlavně písmeno c, které zformátuje dané číslo jako měnu podle místního nastavení systému. Takže String.Format("{0:c}", 1234) vrátí číslo 1234 jako měnu, čili 1 234,00 Kč. Dosazované parametry nemusí být jen čísla, mohou to být texty, datum atd. Zde je tedy kompletní kód procedury události Paint.

    Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
        e.Graphics.Clear(Color.White)

        'zjistit hodnotu nejvyyššího sloupce
        Dim max As Integer = zaznamy(1).hodnota
        For i As Integer = 1 To zaznamy.Length - 1
            If zaznamy(i).hodnota > max Then max = zaznamy(i).hodnota
        Next

        'vykreslit graf
        Dim rozestup As Integer = 800 / (zaznamy.Length + 1)
        Dim vyska As Integer = 500
        Dim sirka As Integer = rozestup * 0.25
        Dim pozadi As New Drawing2D.LinearGradientBrush(New Point(0, 60), New Point(0, 600), Color.LightBlue, Color.Blue)
        For i As Integer = 0 To zaznamy.Length - 1
            Dim x As Integer = (i + 1) * rozestup
            Dim y As Integer = zaznamy(i).hodnota / max * vyska

            'vykreslit sloupec
            e.Graphics.FillRectangle(pozadi, x - sirka, 580 - y, sirka * 2, y)

            'vykreslit popisky pod sloupce
            DrawText(e.Graphics, zaznamy(i).Text, x, 590)

            'vykreslit částky nad sloupce
            Dim txt As String = String.Format("{0:c}", zaznamy(i).hodnota)
            DrawText(e.Graphics, txt, x, 580 - y - 40, -60)
        Next
    End Sub

Hotový graf

To je pro dnešek vše. Zopakovali jsme načítání ze souboru, naučili se struktury, transformace a vykreslování textu.

Nejste již úplní začátečníci, takže jsem dnes nerozebíral úplně všechny detaily, nepopisoval jsem podrobně, k čemu kde jaký parametr slouží. Pokud něco není jasné, pište do diskuse.

Jako domácí úkol si můžete zkusit napsat program, kterému zadáte text a on spočítá, kolikrát se v tomto textu vyskytují jednotlivá písmena (nerozlišujeme velká a malá). Výsledek program vyexportuje do textového souboru tak, aby jej byl schopen načíst a zobrazit náš program pro zobrazování grafů. Rozlišujte diakritiku, ignorujte znaky, které nejsou písmena. Hodí se funkce Char.IsLetter, která vrátí True, pokud je daný znak písmeno.

Grafům četnosti výskytů jednotlivých znaků se říká histogramy, využívají se například při prolamování primitivnějších šifer. Můžete se také podívat na odlišnosti histogramů v různých jazycích.

 

hodnocení článku

6 bodů / 6 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Mohlo by vás také zajímat

Tipy a triky pro ASP.NET - díl 5.: Komponenta pro zadávání data a času

V tomto díle se podíváme na to, jak s využitím komponenty CalendarExtender z knihovny AjaxControlToolkit napsat komponentu pro snadné zadávání data, která podporuje validaci a datové zdroje.

Programování Windows Services

Services (služby) jsou jedním ze základních stavebních kamenů programování pro systémy Windows. V článku se budu věnovat jak obecným principům a doporučením, tak konkrétním implementačním postupům.

Windows Presentation Foundation (WPF) - díl 6.: Základy pozicování

Pozicování je velká přednost technologie WPF. Dovoluje připravovat dynamické rozložení prvků s předvídatelným chováním při změně nejen velikosti okna, ale i elementů uvnitř něj. V tomto díle se věnuji základním principům pozicování.

 

 

Nový příspěvek

 

Mr.

1'

csharp|

xml|

css|


'

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Diskuse: Vykreslujeme graf

Dobrý den,

snažil jsem se pomocí VB vykreslit osy pictureboxu a rastr, udělal jsem si třídu a funkci která to vykreslí.

zde volám po stisknutí tlačítka funkce


 Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        'osy.osy(g, PictureBox1.Width, PictureBox1.Height, pen, True, True)

        osy.pozadi(PictureBox1.Width, PictureBox1.Height, pen, g)



    End Sub

Tady v modulu definuju vlastnosti


Module Module1



    Class kresleni
        Sub New()
        End Sub

        Public Sub pozadi(ByVal PB_width As Integer, ByVal PB_height As Integer, ByVal pero As Drawing.Pen, ByVal g As Graphics)

            Dim pozadi As New Drawing2D.LinearGradientBrush(New Point(0, PB_height), New Point(0, 0), Color.LightBlue, Color.WhiteSmoke)

            'vykreslit(sloupec)
            g.FillRectangle(pozadi, 0, 0, CSng(PB_width), CSng(PB_height))
            g.DrawRectangle(pero, 0, 0, CInt(PB_width), CInt(PB_height))

        End Sub

        Public Sub osy(ByVal g As Graphics, ByVal PB_width As Integer, ByVal PB_height As Integer, ByVal pero As Drawing.Pen, ByVal X_zapnuto As Boolean, ByVal Y_zapnuto As Boolean)


            Dim pero_osy As New Drawing.Pen(Color.Red, 1)



            If X_zapnuto = True Then
                'osa x
                g.DrawLine(pero_osy, PB_width, CInt(PB_height / 2), 0, CInt(PB_height / 2))

                For i As Integer = 0 To PB_width
                    pero_osy = Pens.DarkGray
                    g.DrawLine(pero_osy, CInt(PB_width / 2) + 10 * i, 0, CInt(PB_width / 2) + 10 * i, PB_height)
                    g.DrawLine(pero_osy, CInt(PB_width / 2) - 10 * i, 0, CInt(PB_width / 2) - 10 * i, PB_height)

                    pero_osy = Pens.Red
                    If i Mod 2 = 1 Then
                        g.DrawLine(pero_osy, CInt(PB_width / 2) + 10 * i, CInt(PB_height / 2) - 10, CInt(PB_width / 2) + 10 * i, CInt(PB_height / 2) + 10)
                        g.DrawLine(pero_osy, CInt(PB_width / 2) - 10 * i, CInt(PB_height / 2) - 10, CInt(PB_width / 2) - 10 * i, CInt(PB_height / 2) + 10)
                    Else
                        g.DrawLine(pero_osy, CInt(PB_width / 2) + 5 * i, CInt(PB_height / 2) - 5, CInt(PB_width / 2) + 5 * i, CInt(PB_height / 2) + 5)
                        g.DrawLine(pero_osy, CInt(PB_width / 2) - 5 * i, CInt(PB_height / 2) - 5, CInt(PB_width / 2) - 5 * i, CInt(PB_height / 2) + 5)
                    End If

                Next
            End If


            If Y_zapnuto = True Then
                'osa y
                g.DrawLine(pero_osy, CInt(PB_width / 2), 0, CInt(PB_width / 2), PB_height)
                'stupnice osy y
                For i As Integer = 1 To PB_width

                    pero_osy = Pens.DarkGray
                    g.DrawLine(pero_osy, 0, CInt(PB_height / 2) + 10 * i, PB_width, CInt(PB_height / 2) + 10 * i)
                    g.DrawLine(pero_osy, 0, CInt(PB_height / 2) - 10 * i, PB_width, CInt(PB_height / 2) - 10 * i)
                    'g.DrawLine(pero_osy, 0, 10 * i, PB_width, 10 * i)
                    pero_osy = Pens.Red

                    If i Mod 2 = 1 Then
                        g.DrawLine(pero_osy, CInt(PB_width / 2) - 10, CInt(PB_height / 2) + 10 * i, CInt(PB_width / 2) + 10, CInt(PB_height / 2) + 10 * i)
                        g.DrawLine(pero_osy, CInt(PB_width / 2) - 10, CInt(PB_height / 2) - 10 * i, CInt(PB_width / 2) + 10, CInt(PB_height / 2) - 10 * i)
                    Else
                        g.DrawLine(pero_osy, CInt(PB_width / 2) - 5, CInt(PB_height / 2) + 5 * i, CInt(PB_width / 2) + 5, CInt(PB_height / 2) + 5 * i)
                        g.DrawLine(pero_osy, CInt(PB_width / 2) - 5, CInt(PB_height / 2) - 5 * i, CInt(PB_width / 2) + 5, CInt(PB_height / 2) - 5 * i)
                        'g.DrawLine(pero_osy, CInt(PB_width / 2) - 5, 10 * i, CInt(PB_width / 2) + 5, 10 * i)
                    End If

                Next

            End If

        End Sub
    End Class
End Module

Při stisknutí hlásí chybu NullReferenceException u každé vykreslovací funkce, a přitom ještě před pár řádky vše fungovalo v pořádku, nejspíše jsem nějakou část kódu omylem smazal a to působí tyto problémy.

nahlásit spamnahlásit spam 0 / 2 odpovědětodpovědět

Diskuse: Vykreslujeme graf

Zdravím, srozumitelnost je myslím v pořádku, ale zajímalo by mne jak by měl být zobrazen čárový graf? Díky za odpověď

nahlásit spamnahlásit spam -2 / 2 odpovědětodpovědět

Nevím přesně, co myslíte čárovým grafem. Někdo tak nazývá "sloupcový graf s čárami místo sloupců", někdo spojnicový graf, někdo "odskákaný" čárový graf.

Zkuste upravit metodu Form1_Paint třeba takto:

Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
        e.Graphics.Clear(Color.White)

        'zjistit hodnotu nejvyyššího sloupce
        Dim max As Integer = zaznamy(1).hodnota
        For i As Integer = 1 To zaznamy.Length - 1
            If zaznamy(i).hodnota > max Then max = zaznamy(i).hodnota
        Next

        'vykreslit graf
        Dim rozestup As Integer = 800 / (zaznamy.Length + 1)
        Dim vyska As Integer = 500
        Dim sirka As Integer = rozestup * 0.25
        Dim pero As New Drawing.Pen(Color.Red, 3)
        Dim pero1 As New Drawing.Pen(Color.Green, 3)
        Dim x0 As Integer = 0
        Dim y0 As Integer = 0

        Dim pozadi As New Drawing2D.LinearGradientBrush(New Point(0, 60), New Point(0, 600), Color.LightBlue, Color.Blue)

        For i As Integer = 0 To zaznamy.Length - 1
            Dim x As Integer = (i + 1) * rozestup
            Dim y As Integer = zaznamy(i).hodnota / max * vyska
            

            'vykreslit sloupec
            e.Graphics.FillRectangle(pozadi, x - sirka, 580 - y, sirka * 2, y)

            If i > 0 Then
                e.Graphics.DrawLine(pero, x0, 580 - y0, x, 580 - y)
            End If

            e.Graphics.DrawLine(pero1, CInt(x - rozestup / 2), 580 - y0, CInt(x - rozestup / 2), 580 - y)
            e.Graphics.DrawLine(pero1, CInt(x - rozestup / 2), 580 - y, CInt(x + rozestup / 2), 580 - y)

            'vykreslit popisky pod sloupce
            DrawText(e.Graphics, zaznamy(i).Text, x, 590)

            'vykreslit částky nad sloupce
            Dim txt As String = String.Format("{0:c}", zaznamy(i).hodnota)
            DrawText(e.Graphics, txt, x, 580 - y - 40, -60)
            x0 = x
            y0 = y
        Next
    End Sub

nahlásit spamnahlásit spam 0 / 2 odpovědětodpovědět

Díky za typ jistě to vyzkouším. Měl jsem na mysli graf spojnicový tedy v tomto případě hodnoty obratu spojené čarou bez sloupců. Myslím, že si rozumíme.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Takže něco jako červená čára v mém příkladu? Článek, pod kterým se nacházíme, je velmy umným a srozumitelným návodem, jak 1. získat data pro grafické vyobrazení (jeden z mnoha způsobů) a jak je zpracovat, a 2. jakou filosofií dostat tato data na obrazovku. A to, co si na ni skutečně nakreslíme, je již plně věcí invence, představivosti a potřeb programátora - princip je dost opdobný až stejný.

Jediným zásadním rozdílem tvorby spojnicového a sloupcového grafu je skutečnost, že u grafů typu sloupcového pracujeme v dané iteraci vždy s jednou hodnotou, u spojnicového si musíme zapamatovat i tu hodnotu minulou, abychom věli "odkud" vést čáru (to jsou ty hodnoty x0 a y0). Pokud se jedná konkrétně už jen o spojnicový graf, šel bych na něj trochu jinak. Jistě jste si všiml, že (zvláště pokud zvětšíte šířku pera) ta čára ne zrovna pěkně navazuje. Proto bych pouřil speciální metodu pro kreslení soustavy čar a tam se postup trošičku obrátí - nejprve zjistíme všechny body a pak teprve vykreslíme celý polygon jako celek. Např. (opět vycházím z projektu v článku, pouze změňte proceduru Form1_Paint):

Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
        e.Graphics.Clear(Color.White)

  'zjistit hodnotu nejvyyššího sloupce
  Dim max As Integer = zaznamy(1).hodnota
  For i As Integer = 1 To zaznamy.Length - 1
     If zaznamy(i).hodnota > max Then max = zaznamy(i).hodnota
  Next



  'vykreslit graf
  Dim rozestup As Integer = 800 / (zaznamy.Length + 1)
  Dim vyska As Integer = 500

  ' nadefinujeme barvu a tloušťku pera
  dim pero2 as New Drawing.Pen(Color.DeepPink, 10)

  ' na konci čáry upravíme tvar zakončení (ať to vypadá...)
  pero2.EndCap = Drawing2D.LineCap.Round

  ' totéž pro začátek čáry
  pero2.StartCap = Drawing2D.LineCap.Round

  ' a ještě jednou zaoblíme i přechody jednotlivých segmentů,
  ' jinak budou sice propojené, ale špičaté
  pero2.LineJoin = Drawing2D.LineJoin.Round

  ' definujeme body křivky
  Dim body(zaznamy.Length - 1) As Point

  For i As Integer = 0 To zaznamy.Length - 1
     body(i) = New Point((i + 1) * rozestup, 580 - zaznamy(i).hodnota / max * vyska)
  Next

 ' polygon vykreslíme
  e.Graphics.DrawLines(pero2, body)


  ' cyklus zůstane, vykreslí úpouze popisky
  For i As Integer = 0 To zaznamy.Length - 1
     Dim x As Integer = (i + 1) * rozestup
     Dim y As Integer = zaznamy(i).hodnota / max * vyska
            
     'vykreslit popisky pod sloupce
     DrawText(e.Graphics, zaznamy(i).Text, x, 590)

     'vykreslit částky nad sloupce
     Dim txt As String = String.Format("{0:c}", zaznamy(i).hodnota)
     DrawText(e.Graphics, txt, x, 580 - y - 40, -60)
  Next
        
End Sub

Samozřejmě těď ještě dodělat nějaké osy případně mřížku...

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Dobry den, rad bych videl, jak se delaji ty osy a mrizka :) Dekuji

nahlásit spamnahlásit spam -2 / 2 odpovědětodpovědět

Zdravím, s tím čárovým grafem jsem to překombinoval, vím, že jsem jej v minulém díle slíbil, ale z důvodu rozsahu jsem se k němu v tomto článku nedostal. Navíc to nebylo příliš šťastně řečeno, měl jsem na mysli graf spojnicový.

nahlásit spamnahlásit spam 0 / 2 odpovědětodpovědět

O čem chystáte další díl, smím-li se zeptat?

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Záměrně jsem to do článku nepsal, protože to nevím. Budu muset něco vymyslet.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Což takhle práce s sql databází?

nahlásit spamnahlásit spam -2 / 2 odpovědětodpovědět

Pěkné články, děkuji.

Také bych se přimlouval za práci s DB. Ev. méně typické, ale potřebné věci - jako např. text do pdf, email, porty...

nahlásit spamnahlásit spam -2 / 2 odpovědětodpovědět

Diskuse: Vykreslujeme graf

Dobry den, mohl bych se zeptat jak napsat prikaz, ktery mi nakresli treba primku, ale ne v udalosti paint, ale treba button1.click?

predem dekuji za odpovedi.

nahlásit spamnahlásit spam -2 / 2 odpovědětodpovědět

Diskuse: Vykreslujeme graf

Dobrej.

Opravdu skvělý seriál, sice to moc nechápu ale ono se to časem dostaví :-)

Bylo by ovšem dobré, kdyby byl nekonci článku ke stažení celý programový kod programu. Občas se mi stávalo, že jsem přemýšlel co tam patří a co ještě ne... A také pro kontrolu když něco nejde tak jak má...

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Diskuse: Vykreslujeme graf

Zdravím, začal jsem se studiem těchto článků včera a je to můj úplně první kontakt s VB. Články úplně super díky moc.

Při plnění úkolu s počtem výskutu písmen jsem se do toho nějak zamotal a za boha ho (myšleno script) nemůžu donutit aby mi vypsal data do souboru "prograf.txt" už jsem si ty data vypsal i do ListView abych si byl jist, že je mám... a mám. Pokusil jsem se to přesně opsat podle vzoru s "domácím účetnistvím" ale někde je chyba možná musím někde něco nastavit a nevím co..... přitom mi to žádné chyby nehází....

tedy je komplet kód:

Public Class Form1

    Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
        'uložit data
        Dim soubor As New IO.StreamWriter("prograf.txt")           'otevřít soubor
        For i As Integer = 0 To ListView1.Items.Count - 1           'projít všechny položky seznamu
            soubor.WriteLine(ListView1.Items(i).SubItems(0).Text)   'zapsat datum
            soubor.WriteLine(ListView1.Items(i).SubItems(1).Text)   'zapsat částku
        Next
        soubor.Close()
    End Sub
    Structure polozka
        Dim Pismeno As String
        Dim Vyskyt As Integer
    End Structure
    Dim polozky() As polozka
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        'otevřít soubor pro čtení
        Dim sr As New IO.StreamReader("pismena.txt", System.Text.Encoding.Default)

        'načíst počet záznamů v souboru
        Dim pocet As Integer = CInt(sr.ReadLine())
        Dim text As String = TextBox1.Text
        Dim pocetZn As String = text.Length



        'nadimenzovat pole se záznamy
        ReDim polozky(pocet - 1)

        'projít v cyklu záznamy a načítat hodnoty ze souboru do pole
        For i As Integer = 0 To pocet - 1
            polozky(i).Pismeno = sr.ReadLine()     'načíst písmeno
            polozky(i).Vyskyt = 0
            For j As Integer = 0 To pocetZn - 1
                Dim Box As String = Char.ToUpper(text(j))
                Dim Box2 As String = Char.ToUpper(polozky(i).Pismeno)
                If (Char.IsLetter(Box) And (Box.Contains(Box2))) Then polozky(i).Vyskyt = polozky(i).Vyskyt + 1
            Next

            Dim list As New ListViewItem()                   'vytvořit novou položku seznamu
            list.Text = polozky(i).Pismeno                                'vypsat datum
            list.SubItems.Add(polozky(i).Vyskyt)                        'přidat druhý sloupeček s částkou
            ListView1.Items.Add(list)
        Next


        'zavřít soubory
        sr.Close()

    End Sub

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

    End Sub


    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
        ListView1.Items.Clear()

    End Sub

End Class

klasicky jsem se sek a sám na to nepřijdu

díky za pomoc

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

To počítání výskytu znaků je jakési divné, sr.ReadLine načte celý řádek, a ne jen jedno písmeno.

Nebudu zde uvádět celý kód, s tím si musíte pohrát sám. Jak by počítání počtu výskytů znaků mělo vypadat?

1. Vytvoříte si seznam na struktury Polozka, kterou už máte ve své ukázce nadeklarovanou. Dim pismena As List(Of Polozka)

2. Vytvoříte si StreamReader, který bude číst vstupní soubor s nějakým textem, jehož statistiku chcete zobrazit, a jednotlivé řádky čtete v cyklu While Not sr.EndOfStream. Vlastnost EndOfStream má hodnotu True, když dojtete na konec, pokud jsou ještě nějaké další řádky, tak vrací False; Not před tím prohodí True a False, takže While cyklus se opakuje, dokud je ještě v souboru co číst).

3. V každém kroku While cyklu načtete řádek do proměnné (třeba r). Řádek si zmenšíte na malá písmena, aby se velikost písmen nebrala v úvahu - r = r.ToLower(). Ve For cyklu si pro každý řádek (od 0 do r.Length - 1, což je počet písmen v řádku) projdete všechna písmena v proměnné r, dotyčné písmeno zkusíte najít v seznamu pismena (opět ho projdete For cyklem, počet písmen v seznamu zjistíte z pismena.Count), pokud tam už bude, tak mu přičtete jedničku k počtu výskytů, pokud ho nenajdete, vytvoříte novou strukturu a do seznamu ji přidáte přes pismena.Add(novePismeno). Na to prohledávání seznamu, přičítání výskytů a přidávání písmena, v případě že se nenajde, bych doporučil napsat si proceduru PridejVyskyt(pismeno As Char), kterou akorát zavoláte pro každé písmeno v řádku. Ona už si sama seznam písmen projde a upraví počet výskytů, nebo přidá nový záznam.

4. Jakmile takto zpracujete všechny řádky souboru, máte v seznamu pismena počty výskytů všech písmen, které se v textu vyskytly. Pak už z toho krásně můžete udělat graf.

nahlásit spamnahlásit spam 1 / 1 odpovědětodpovědět

Diskuse: Vykreslujeme graf

Dobrý den,

rád bych věděl jestli neznáte nějaký dobrý zdroj informaci na grafy a VB 2005. Literaturu nebo lépe internetový zdroj. Na internetu jsem zatím našel jen placené aplikace na G. grafy a nebo velmi odlišné než potřebuji a k tomu jen do VBA.

Řeším totiž ve své aplikaci zobrazování rozvrhu operací, které mám zatím jen na "výstupu" z pole do Datagridu, pomocí Ganttova diagramu. Taktéž bych ocenil radu jak v takovém případě vůbec začít. Pro představu je můj "formát dat" v poli následující


'cislo operace
Operation_Time(pole_A(Sel_i), 0) = pole_A(Sel_i)

'cislo stroje 
Operation_Time(pole_A(Sel_i), 1) = pole_M(Sel_i)
            
'startovní cas operace 
Operation_Time(pole_A(Sel_i), 2) = pole_E(Sel_i) - pole_P(Sel_i)

'cas dokoncení
Operation_Time(pole_A(Sel_i), 3) = pole_E(Sel_i)
            
Operation_Time(pole_A(Sel_i), 4) = Number_of_end_oper

Byl by to vlastně jaký si druh sloupcového grafu" otočeného na bok" a s číslem stroje místo měsíců jak je naznačeno zde.

Problém je však rozdělit tento sloupec resp. řádek na jednotlivé bloky ohraničené startovním a ukončovacím bodem. Spolu s ohraničením jednotlivých operací např. rámem a číslem operace uvnitř tohoto bloku.

Děkuji předem za radu a navedení na cestu jak řešit tento problém

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Diskuse: Vykreslujeme graf

Dobrý den,

protože jsem natvrdlý, dumám pokaždé co každá proměnná dělá a kde se co bere. Zdá se mi, že pokud bude zjišťování hodnoty MAX pro potřeby stanovení velikosti grafu probíhat od hodnoty 1 v poli, bude opomenuta položka v poli na místě 0:

      'zjistit hodnotu nejvyššího sloupce
        Dim max As Integer = zaznamy(1).hodnota
        For i As Integer = 1 To zaznamy.Length - 1
            If zaznamy(i).hodnota > max Then max = zaznamy(i).hodnota
        Next
        MsgBox(max)'to jsem doplnil pro kontrolu

Předpokládám, že sr.readline() si samo s každým voláním pošoupne řádek o 1 a tudíž v první položce pole není 12. Vim, že je to asi směšná chyba pro profíky, ale pro ty, kteří koukají na VBNET poprvé je to zásek. :) Seriál je bezva. Nechci předbíhat, možná je to v dalším díle. Ale strašně by se mi hodilo, pracovat s daty v tabulce, ne v řádkovém souboru...prostě mít někde uložené pole, a to číst nezávisle (třeba v excelu jako tabulku, nebo i v txt, ale tak, abych ve vlastním programu k němu přistupoval souřadnicově jako k poli a ne složitě pomocí sr.readline).

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Takže popořadě:

S tou chybkou máte pravdu, mělo by to vypadat takto:

'zjistit hodnotu nejvyššího sloupce
        Dim max As Integer = zaznamy(0).hodnota
        For i As Integer = 1 To zaznamy.Length - 1
            If zaznamy(i).hodnota > max Then max = zaznamy(i).hodnota
        Next
        MsgBox(max)'to jsem doplnil pro kontrolu

sr.Readline přečte 1 řádek ze souboru (čte tak dlouho, dokud nenarazí na znak konce řádku nebo na konec souboru) a až PO přečtení nastaví ukazatel (pro další čtení) na začátek nového řádku.

To, že v proměnné zaznamy(0) nemáme 12 ale název měsíce je způsobeno tím, že po otevření souboru pro čtení nejprve přečteme první řádek (Dimpocet as integer=CInt(sr.ReadLine())),

a hodnoty pak čteme v cyklu, ale při vstupu do tohoto cyklu už máme ukazatel čtení v souboru nastaven na začátku druhého řádku.

 'otevřít soubor pro čtení
        Dim sr As New IO.StreamReader("graf.txt", System.Text.Encoding.Default)

        'načíst počet záznamů v souboru
        Dim pocet As Integer = CInt(sr.ReadLine())

        'nadimenzovat pole se záznamy
        ReDim zaznamy(pocet - 1)

        'projít v cyklu záznamy a načítat hodnoty ze souboru do pole
        For i As Integer = 0 To pocet - 1
            zaznamy(i).Text = sr.ReadLine()     'načíst popis
            zaznamy(i).hodnota = CInt(sr.ReadLine())   'načíst částku
        Next

        'zavřít soubor
        sr.Close()

S tím ukládáním dat. Těch možností je nepřeberné množství, ale je zapotřebí si uvědomit několik skutečností:

Jakákoliv manipulace se souborem (na disku) je o několik řádů pomalejší, než práce s daty v paměti. Proto se zpravidla (až na velice vzácné výjimky) postupuje tak, že data mám nějak uložené v souboru - jak to je celkem nezajímavé (pro mne jako uživatele), jde o to, aby to bylo co nejjednodušší a nejrychlejší pro program), jako první krok tato data načtu do paměti (pokud možno co nejjednodušší a nejrychlejší metodou), a pak s nimi v paměti pracuji.

Proto snaha "mít data v souboru uložena nějak tabulkově" má opodstatnění snad pouze v případech, že k nim chcete přistupovat i jinak než svým programem (třeba modifikovat v excelu, nebo přímo texťákem,...)

Další možností ukládání dat je do databází, kde vlastně tabulkově uložena jsou, ale i tam se většinou používá asynchronní přístup - nejprve potřebná data načtu do paměti počítače a až v té paměti s nimi pracuji. Obrovskou výhodou v tomto případě je skutečnost, že do paměti počítače nemusím načíst data všechna, ale pouze ta, která mne zajímají - proto se tento způsob jako jeden z mála hodí pro zpracování obsáhlých kolekcí dat.

Pěkný příklad uchovávání dat v souboru vyšel i v zatím posledním díle VB pro začátečníky.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Díky za reakci. Na poslední díl se už chystám. S Vašim názorem na přístup k souborům souhlasím a hned jak bude čas provedu nějaké samostudium ve smyslu práce i s jinými typy souborů. Současně vycházím ze svého pohledu neprogramátora. Potřebuji nástroj, který běží všude (win XP apod) a bude si pamatovat zadaný postup a ten opakovaně provádět a snadno si ho vyrobím. Množství zpracovávaných dat je zanedbatelné (pár okrajových podmínek nějakého výpočtu). Největší vtip programu spočívá ve vlastním výpočtu-jak modelovat úlohu atd-vlastně spíš matematika. Horší bude, až budu chtít program prodat, to už Vaše argumenty jsou velmi důležité a je tu místo pro profesionálního programátora. Je až překvapující, kolik mých kolegů má takto v jazycích QBASIC, Pascal (co kdo uměl) pro své potřeby napsáno hafo prográmků, které v komerčních verzích stojí desetitisíce.

No nic, co člověk-to názor, jdu na další díl.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Diskuse: Vykreslujeme graf

Jak by šlo prosím udělat, aby šlo použít form u kterého jde měnit velikost a graf se podle toho zmenšoval a zvětšoval. Trochu jsem si s tím hrál, ale nějak mi to nejde. Dík

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Místo ručně zadaných velikostí použijte Me.ClientSize.Width a Me.ClientSize.Height, které vrátí velikost vnitřní plochy formuláře. Dále je třeba do události Form1_Resize dát příkaz Me.Invalidate, aby se při změně velikosti formulář překresloval.

Původně jsem to chtěl do seriálu zahrnout, ale pak jsem si říkal, že bych to již zbytečně komplikoval. I tak si nejsem jist, jestli je i tento díl srozumitelný (nějak se mi nelíbí).

nahlásit spamnahlásit spam 0 / 2 odpovědětodpovědět

Diky. Uz to jde. A ohledne srozumitelnosti clanku si myslim, ze je to v pohode. Jen tak dal.

nahlásit spamnahlásit spam 0 / 2 odpovědětodpovědět

Diskuse: Vykreslujeme graf

Dalsi super dil. Takovou rychlost jsem necekal. Diky

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback