Nepovinné rozšíření
Pokud máte chuť se vrhnout rovnou na komunikaci, můžete tuto kapitolu přeskočit... Dříve než začneme, dovolím si nabídnout malé rozšíření. Aby naše aplikace uměla něco víc, než jen kreslit bílé čáry, přidáme si funkce na změnu tloušťky a barvy štětce. První rozšíříme menu stejně, jako na vidíte na obrázku:
Já jsem použil jedno standardní tlačítko na zvolení barvy a jeden ComboBox pro tloušťku štětce - fantazii se však meze nekladou a výběr můžete realizovat jak chcete.
Pokud se budete držet mého konvenčního řešení, vložte si na formulář
a do tlačítka
Zvolit barvu štetce vložte následující kód:
Private Sub ToolStripMenuItemBarvaStetce_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ToolStripMenuItemBarvaStetce.Click
ColorDialog1.Color = drawColor
If ColorDialog1.ShowDialog() = Windows.Forms.DialogResult.OK Then
drawColor = ColorDialog1.Color
End If
End Sub
Tím máme hotový výběr barvy. Pro zvolení tloušťky použijeme ComboBox pojmenovaný
ToolStripComboSirkaStetce. Dáme na výběr z několika přednastavených hodnot, které do něj vložíme v proceduře
Form1_Load:
For i As Integer = 1 To 10
ToolStripComboSirkaStetce.Items.Add(i.ToString + "px")
Next
ToolStripComboSirkaStetce.SelectedIndex = drawWidth - 1
Přepisování vložených položek zamezíme nastavením vlastnosti
ToolStripComboSirkaStetce.
DropDownStyle na
DropDownList. Aby se při změně výběru změnila i proměnná
drawWidth, přidáme do události
SelectedIndexChanged kód:
Private Sub ToolStripComboSirkaStetce_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ToolStripComboSirkaStetce.SelectedIndexChanged
drawWidth = ToolStripComboSirkaStetce.SelectedIndex + 1
End Sub
To by mělo být vše - barevná kreslící tabule je na světě :)
Synchronizace
Asynchronní přijímání
Pokud použijeme asynchronního přijímání, je to pro nás velmi výhodné. V opačném případě bychom museli totiž pořád kontrolovat, zda přišla nějaká data a na to by padla velká část procesorového výkonu. Celou situaci znázorňuje obrázek:
K tomuto účelu budeme mít proceduru
CtiData. Její volání vypadá takto:
networkStream.BeginRead(Buffer, 0, BUFFER_SIZE, AddressOf CtiData, Nothing)
Čekat na data potřebujeme od vzniku spojení, vložíme jej tedy do procedur
KlientSePripojuje a
Pripojit za příkaz, kde přiřazujeme do NetworkStreamu:
...
networkStream = tcp.GetStream()
networkStream.BeginRead(Buffer, 0, BUFFER_SIZE, AddressOf CtiData, Nothing)
...
Asi jste si všimli, že používáme proměnné
Buffer pro uložení příchozích dat a
BUFFER_SIZE jako definici velikosti bufferu. Jejich deklarace provedeme takto:
Const BUFFER_SIZE As Integer = 512
Dim Buffer(BUFFER_SIZE) As Byte
Nastal čas popsat si funkci procedury
CtiData, která vypadá následovně:
Sub CtiData(ByVal at As System.IAsyncResult)
Try
Dim prijato As Integer = networkStream.EndRead(at)
If prijato < 1 Then Throw New Exception()
tcp.GetStream.BeginRead(Buffer, 0, BUFFER_SIZE, AddressOf CtiData, Nothing)
Catch e As ObjectDisposedException
Catch e As Exception
MsgBox(e.Message)
StripInfo.Text = "Spojení bylo ukončeno"
MsgBox("Spojení ukončeno!", MsgBoxStyle.Information)
OdemkniPolozky()
End Try
End Sub
Příkazem EndRead potvrdíme přijetí dat. Návratová hodnota je počet přijatých bytů. Pokud spojení skončilo, bude rovna nule.
Když se data zpracují, znovu se zavolá BeginRead a vytvoří se tak smyčka, která se provede vždy, když přijdou data.
Tím jsme zvládli přijímání dat. Až do teď byla komunikace dosti obecná, avšak od příští kapitoly se zaměříme přímo na implementaci vlastního protokolu pro kreslící tabuli.
Vše připraveno
Konečně se dostáváme k hlavnímu problému našeho scénáře. Z minulých dílů máme k dispozici uživatelské rozhraní a připravený komunikační kanál
NetworkStream odvozený z TCP spojení. Data umíme přijímat, ale protože nejpřijde nic víc, než hromada bytů, musíme data umět nějak interpretovat. K tomu v jakém formátu je posílat a následně jak je dekódovat na druhé straně navrhneme jednoduchý protokol.
Jak navrhnout protokol?
Pro naše účely nám postačí jednoduchý textový prokol. Příkazy budeme oddělovat
středníkem a parametry mezerou. Takže například příkaz s dvěma argumenty může vypadat takto:
příkaz parametr parametr;
Více příkazů pak takhle:
příkaz parametr1 parametr2;jiný_příkaz parametr;ještě_jiný_příkaz_bez_parametru;
Aby jsme mohli příkazy textově odesílat, napíšeme si proceduru
PosliPrikaz:
Sub PosliPrikaz(ByVal prikaz As String)
Dim Connected As Boolean = False
If Not tcp Is Nothing Then
If Not tcp.Client Is Nothing Then
Connected = tcp.Connected
End If
End If
If Connected = False Then Exit Sub
Try
Dim bfr(prikaz.Length - 1) As Byte
bfr = System.Text.Encoding.ASCII.GetBytes(prikaz)
networkStream.Write(bfr, 0, bfr.Length)
Catch e As ObjectDisposedException
Catch ex As Exception
StripInfo.Text = "Spojení bylo ukončeno"
tcp.Close()
MsgBox("Spojení ukončeno!", MsgBoxStyle.Information)
OdemkniPolozky()
End Try
End Sub
Když už známe, jak budou příkazy strukturované, můžeme vyplnit prázdné místo v proceduře
CtiData.
POZOR: Data přes TCP/IP mohou dorazit po částech nebo naopak více najednou spojených za sebe! Z toho důvodu budeme příchozí byty konvertovat na řetězec a přidávat do proměnné
text_buffer. Deklaraci přidáme k ostatním
bufferům:
Const BUFFER_SIZE As Integer = 512
Dim Buffer(BUFFER_SIZE) As Byte
Dim text_buffer As String
A do
CtiData vepíšeme parser na příkazy, který bude odsekávat kompletní příkazy zakončené středníkem a posílat je do funkce
ProvedPrikaz(prikaz):
text_buffer += System.Text.Encoding.ASCII.GetString(Buffer, 0, prijato)
Do While text_buffer.Contains(";")
Dim poziceStredniku As Integer = text_buffer.IndexOf(";")
Dim prikaz As String = text_buffer.Substring(0, poziceStredniku)
text_buffer = text_buffer.Substring(poziceStredniku + 1)
ProvedPrikaz(prikaz)
Loop
Parser máme už hotový, odesílání připravené, už nám jen stačí napsat funkci
ProvedPrikaz(prikaz), co bude vyhodnocovat příkazy. Nejdříve si je ale nadefinujeme.
Příkazy protokolu
Nakonec nám budou stačit pouze dva příkazy:
- smazat; - smaže tabuli
- cara x1 y1 x2 y2 sirka r g b; - nakreslí čáru
Abychom mohli vykreslovat příkaz čára, napíšeme si novou funkci:
Sub KresliCaru(ByVal x1 As Integer, ByVal y1 As Integer, ByVal x2 As Integer, ByVal y2 As Integer, ByVal drawWidth As Integer, ByVal color As Color)
Threading.Monitor.Enter(Me)
grp.FillEllipse(New Pen(color).Brush, New RectangleF(x1 - drawWidth / 2, y1 - drawWidth / 2, drawWidth, drawWidth))
grp.DrawLine(New Pen(color, drawWidth), x1, y1, x2, y2)
grp.FillEllipse(New Pen(color).Brush, New RectangleF(x2 - drawWidth / 2, y2 - drawWidth / 2, drawWidth, drawWidth))
PicTabule.Invalidate()
Threading.Monitor.Exit(Me)
End Sub
Všimněte si, že jsem použil příkazy Threading.Monitor.Enter(Me) a Threading.Monitor.Exit(Me). Ty slouží k uzamčení objektu pro určité vlákno. Pokud použiji Enter na určitý objekt, jiné vlákno to nemůže udělat dokud nepoužiji Exit V tomto případě to bude zabraňovat kolizím v zápisu na objekt grp.
Způsob synchronizace
První, co člověka (resp. programátora) v naší situaci napadne, je posílat vše, co jsme nakreslili tomu druhému a nechat ať to vykreslí i u sebe. Bohužel to tak snadné není.
Představte si situaci, kdy oba najednou nakreslí čáru. Vykreslí ji tedy u sebe a odešlou po síti. A když jim oběma přijde nazpět čára toho druhého, nakreslí se přes jejich původní. Asi takhle:
Naštěští to jde celkem snadným způsobem vyřešit. Všechna data půjdou nejdřív na server a teprve ten je bude posílat zpět společně se svými k vykreslení. Funkci serveru lze pak znázornit takto:
Tím pádem bude klient místo vykreslování u sebe vše jen posílat serveru a ten všechnu příchozí komunikaci přesměruje zpět na klienta. Díky tomu bude stejné vykreslovací pořadí i u serveru i u klienta.
Dokončení
Zde je konečně kód, který zpracuje samotný příkaz od parseru:
Sub ProvedPrikaz(ByVal prikaz As String)
Try
Dim casti As String() = Split(prikaz, " ")
Select Case casti(0)
Case "smazat"
Threading.Monitor.Enter(Me)
grp.Clear(Color.Black)
PicTabule.Invalidate()
Threading.Monitor.Exit(Me)
If Status = StatusPripojeni.Server Then PosliPrikaz(casti(0) + ";")
Case "cara"
KresliCaru(Val(casti(1)), Val(casti(2)), Val(casti(3)), Val(casti(4)), _
Val(casti(5)), _
Color.FromArgb(Val(casti(6)), Val(casti(7)), Val(casti(8))))
If Status = StatusPripojeni.Server Then
PosliPrikaz(casti(0) + " " + casti(1) + " " + casti(2) + " " + casti(3) + " " + casti(4) + " " + casti(5) + " " + casti(6) + " " + casti(7) + " " + casti(8) + ";")
End If
Case Else
Throw New Exception
End Select
Catch e As Exception
StripInfo.Text = "Spojení bylo ukončeno"
tcp.Close()
MsgBox("Chybná syntaxe příkazů!", MsgBoxStyle.Information)
OdemkniPolozky()
End Try
End Sub
A teď posílání nakreslených čar. Jako první si vložte funkci, která zjistí jestli jsem klient a tedy jestli nemám vykreslovat, dokud mi to nepřijde od serveru:
Function JsemPripojenyKlient() As Boolean
Dim Connected As Boolean = False
If Not tcp Is Nothing Then
If Not tcp.Client Is Nothing Then
Connected = tcp.Connected
End If
End If
Return Connected And (Status = StatusPripojeni.Klient)
End Function
Následuje upravená procedura pro tlačítko mazání tabule:
Private Sub ToolStripMenuItem_SmazatTabuli_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ToolStripMenuItem_SmazatTabuli.Click
If JsemPripojenyKlient() = False Then
Threading.Monitor.Enter(Me)
grp.Clear(Color.Black)
PicTabule.Invalidate()
Threading.Monitor.Exit(Me)
End If
PosliPrikaz("smazat;")
End Sub
A nakonec to nejlepší, upravené procedury pro kreslení čar. Přidal jsem příkaz na odesílání dat a podmínku, která zabrání vykreslování v případě že je aplikace připojena jako klient.
Private Sub PicTabule_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles PicTabule.MouseDown
If e.Button = Windows.Forms.MouseButtons.Left Then
PosledniBod = e.Location
If JsemPripojenyKlient() = False Then
Threading.Monitor.Enter(Me)
grp.FillEllipse(New Pen(drawColor).Brush, New RectangleF(e.X - drawWidth / 2, e.Y - drawWidth / 2, drawWidth, drawWidth))
PicTabule.Invalidate()
Threading.Monitor.Exit(Me)
End If
Kresleni = True
PosliPrikaz("cara " + e.X.ToString + " " + e.Y.ToString + " " + e.X.ToString + " " + e.Y.ToString + " " + drawWidth.ToString + " " + drawColor.R.ToString + " " + drawColor.G.ToString + " " + drawColor.B.ToString + ";")
End If
End Sub
Private Sub PicTabule_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles PicTabule.MouseMove
If e.Button = Windows.Forms.MouseButtons.Left And Kresleni = True Then
If JsemPripojenyKlient() = False Then
Threading.Monitor.Enter(Me)
grp.DrawLine(New Pen(drawColor, drawWidth), PosledniBod, e.Location)
grp.FillEllipse(New Pen(drawColor).Brush, New RectangleF(e.X - drawWidth / 2, e.Y - drawWidth / 2, drawWidth, drawWidth))
PicTabule.Invalidate()
Threading.Monitor.Exit(Me)
End If
PosliPrikaz("cara " + PosledniBod.X.ToString + " " + PosledniBod.Y.ToString + " " + e.X.ToString + " " + e.Y.ToString + " " + drawWidth.ToString + " " + drawColor.R.ToString + " " + drawColor.G.ToString + " " + drawColor.B.ToString + ";")
PosledniBod = e.Location
End If
End Sub
Pokud jste to vydrželi až do teď, tak děkuji za pozornost a trpělivost. Doufám, že vám článek něco přinesl. V případě jakýchkoliv dotazů se na mě obraťte v diskuzi.
Výsledný kód je ke stažení zde: