Prekreslovani obrazovky   zodpovězená otázka

VB.NET, WinForms, Office

Zdravim

rad bych se zeptal jak resite prekreslovani obrazovky kdyz delate nejakou dlouho bezici operaci a uzivatel vam prepina z Vaseho programu napr. na internet nebo do jinych aplikaci a kdyz se koukne zpatky na tu Vasi aplikaci, tak nektere mista nejsou aktualizovane - typicky, tam kde jsou butony apod?

Mam totiz program, ktery provadi zapis do Excelu, formatuje bunky, zarovnava, pocita a kdyz uzivatel navoli moc zaznamu, tak vytvoreni sesitu, vlozeni hodnot, naformatovani, vypocet vzorcu muze trvat (dle PC) od 40 sec do 2,5 min. Coz samozrejme malokdo vydrzi, jen se divat na ProgressBar a tak se prepina a prepina.

Znam metodu DoEvents, ale nekde jsem cetl ze docekla dost zbrzduje provadeny kod.

Diky za rady Premek

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

Nevím, možná někdo poradí lepší způsob, ale osobně si myslím, že jedna z mála cest (ne-li jediná) je právě přes to

Application.DoEvents()

Překreslení formu je totiž reakce právě na událost, která tvrdí, že nastal stav, kdy je nutno něco na formu překreslit (odkryli jsme ho, zvětšili,...) No a samozřejmě, pokud pro obsloužení této události nedáme programu prostor, neudělá nám to. (nakolik to povede ke zpomalení si můžete sám změřit a můžete nám výsledek i sdělit-bylo by to zajímavé, názory na zatížení systému se totiž liší.

Druhou možností, kterou máte, je poslat Váš časově náročný výpočet do separátního vlákna, pak se hlavní vlákno (s Vaším formulářem) bude nerušeně i nadále starat o své události. Tady ale musíte provést dostatečná opatření (odporuje-li to logice Vaší aplikace), aby Vám uživatel nemohl v průběhu jednoho výpočtu spustit druhý, třetí,...

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

To, že Application.DoEvents zpomaluje prováděnou úlohu, je sice pravda, ale z vlastní zkušenosti je zpomalení tak o 1 procento, což není nijak moc (a záleží také, jak často to voláte, tenhle údaj jsem naměřil ve hře, kde se DoEvents volalo asi 60x za sekundu).

Ale lepší řešení je tu operaci spustit ve vlákně, je zde ale další problém, jelikož z jiného vlákna nelze přistupovat ke komponentám na formuláři. Kód bude vypadat takto:

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        'spustit dlouhou proceduru
        Dim t As New Threading.Thread(AddressOf DlouhaOperace)
        t.Start()
    End Sub

    Sub DlouhaOperace()
        For i As Integer = 1 To 1000
            'provést práci

            'nastavit ProgressBar (jsme v jiném vlákně, nelze to udělat přímo: ProgressBar1.Value = ...
            SetProgress(i / 10)
        Next
    End Sub

    'kvůli volání Invoke musíme nadeklarovat delegáta (stejný jako procedura, ale na začátku je Delegate a pak teprve deklarace, název musí být jiný
    Delegate Sub SetProgressDelegate(ByVal i As Integer)

    'procedura nastavující hodnotu ProgressBaru
    Sub SetProgress(ByVal i As Integer)
        If Me.InvokeRequired Then
            'pokud voláme z jiného vlákna, musíme udělat Invoke
            Me.Invoke(New SetProgressDelegate(AddressOf SetProgress), i)
        Else
            'pokud již nejsme v jiném vlákně, nastavíme hodnotu
            ProgressBar1.Value = i
        End If
    End Sub

Procedura tlačítka vytvoří nové vlákno a spustí s ním proceduru DlouhaOperace. Tam proveďte generování tabulky a občas zavolejte SetProgress s parametrem, který se dosadí jako hodnota ProgressBaru.

Samotná funkce SetProgress nastavuje hodnotu ProgressBaru, ale protože to není možné z jiného vlákna (aby nenastávaly konflikty), musí se zavolat Invoke, který způsobí, že se zavolá předaná procedura z vlákna, které k ProgressBaru přistupovat může. Tento Invoke neudělá nic jiného, že zavolá tu samou proceduru s příslušným parametrem, ale protože poběží už ve vlákně formuláře, provede se druhá větev podmínky. Asi bude nejlepší, když na toto téma napíšu článek.

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

Panove diky za rady

jelikoz delam ve VB .NET teprve par mesicu a s vlakny nemam absolutne zadnou zkusenost, zustanu tedy u DoEvents.

Jednou jsem delal testovaci proceduru na DoEvents, kde jsem udelal

jednoduchy cyklus For -Next a jednou s DoEvents a jednou bez a vysledek byl priblizne (uz si nepamatuji presne) takovy

bez DoEvents = cca 0,8 sec

s DoEvents = cca 10 sec

kody vypadl takto

For i = 1 To 1000000
  a = a + 1
Next i

For i = 1 To 1000000
  DoEvents  
  a = a + 1
Next i

Jeste je treba rict, ze jsem to zkousel ve VBA

Premek

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

To je právě ta chyba - voláte to moc často. Přičtení jedničky je velmi triviální operace a zabere tedy méně času než samotné DoEvents. Pokud byste to volal jen pokud i je dělitelné tisícem (If i Mod 1000 = 0), aplikace se sekat vůbec nebude (sčítání je opravdu velmi rychlá operace) a bude to určitě hotovo do 1 sekundy.

Záleží totiž, jak moc je jeden krok cyklu náročný. Pokud tam dáte triviální operaci, DoEvents nesmíte volat v každém kroku. Překreslování ale triviální není a zabere dost času, takže tam to uplatnit půjde. Pokud by to zpomalovalo, nevolejte jej tak často.

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

Většinou se v takových případech dá pro vložení doevents() využít vhodného místa v programu (často se právě pro tabulkové výpočty používá různých vnořených cyklů: zpracováváte postupně řádky (vnější cyklus) a v každém řádku postupně sloupce (vnitřní cyklus), pak je dobré doevents umisťovat do toho vnějšího cyklu (stejně jako změna stavu Vámi zmiňovaného progressbaru je málokdy potřebná po každé spočítané buňce, protože to představuje stejné zdržení).

Ale po přečtení příspěvku pana Hercega mne zajímalo, jaký vliv na výkonnost bude mít to, že, protože zde nemáme vnořených cyklů a samotný příkaz cyklu je velice rychlý, spouštíme doevents() na základě testu if. Naše aplikace totiž sice v každém průchodu nemusí volat doevents(), ale zato musí testovat podmínku IF.

Pro ty, které to zajímá, uvádím výsledky:

testovací smyčky vypadaly následovně:

Public Class Form1
    Dim a As Long
    Dim mez As Long = 1000000
    Dim cas As DateTime

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        a = 0
        cas = Now
        For i As Long = 0 To mez
            a += 1
        Next
        Label1.Text = Now.Subtract(cas).ToString
    End Sub

    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
        a = 0
        cas = Now
        For i As Long = 0 To mez
            a += 1
            Application.DoEvents()
        Next
        Label2.Text = Now.Subtract(cas).ToString
    End Sub

    Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
        a = 0
        cas = Now
        For i As Long = 0 To mez
            a += 1
            If i Mod 1000 = 0 Then Application.DoEvents()
        Next
        Label3.Text = Now.Subtract(cas).ToString
    End Sub
End Class

a výsledky:

- pro čistou smyčku (pod Button1) celkový čas 0,016 s

- pro smyčku se soustavným voláním doevents (pod Button2) čas 22,7 s

- pro smyčku s podmíněným voláním doevents(pod Button3) čas 0,031 s

Pokud ale celou aplikaci zkompiluji, pak jsou výsledky "trošičku" rozdílné a to tak, že:

- dle button 1 je čas tímto způsobem zjištěný pod hranicí přesnosti měření

- dle button 3 se mi pohybuje na této hraníci (kolem 0,016 s, což je asi nejmenší rozlišení tohoto způsobu měření času)

- dle BUTTON 2 jsem pak dosáhl celkového času 1,06 s

Prosím čtenáře, ať si každý udělá svůj obrázek o použitelnosti jednotlivých přístupů

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

Někdy by se hodil opak než dávat prostor k překreslování obrazovky, tj. ve vhodnou chvíli vykreslování pozastavit. VBA to umí s "Application.ScreenUpdating". Existuje něco takového v NET?

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

Tohle v .NETu nefunguje, ale dá se to třeba udělat tak, že vytvoříte objekt Bitmap požadované velikosti, z něj si uděláte kreslící plátno Graphics, kreslíte vše do něj (což se hned nezobrazuje na obrazovce), a pak to překreslíte na cílové plátno.

Dim b As New Bitmap(400, 400)   '400x400 pixelů
Dim g As Graphics = Graphics.FromImage(b)   'Vytvořit plátno z obrázku
'vykreslovat objekty
g.DrawEllipse ...
'překreslit obrázek z paměti na formulář
e.Graphics.DrawImage(b, 0, 0)

Tady si nejsem jistý, jestli náhodou objekt Graphics takovouhle funkcionalitu nemá, pokud má, je lepší použít obecné řešení a nešaškovat s vlastním obrázkem. Zjistím to a pokud bych něco našel, tak to sem přidám.

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

Myslím, že by tou funkčností mohl být zabudovaný buffering. Je to tak jak to píšete, ale nekreslí se do obrázku, ale do bufferu, který se pak vyplázne na obrazovku najednou. Nevím, jestli to používám správně, ale ve své aplikaci to mám použito nějak takto (z celého mého řešení jsem pro jednoduchost vyseparoval pouze tu otázku bufferingu):

Public Class Form1
    Private form_CurCont As BufferedGraphicsContext
    Private form_Buffer As BufferedGraphics


    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        form_CurCont = BufferedGraphicsManager.Current
        form_Buffer = form_CurCont.Allocate(Me.CreateGraphics, Me.DisplayRectangle)
    End Sub


    Private Sub Form1_Shown(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Shown
' TOTO JE POUZE PRO PŘEDVEDENÍ FUNKČNOSTI:
        Dim x As Double = 0                 ' procentuální průběh animace
        Dim casStartu As Date = Now         ' čas započetí animace
        Dim trvani As Integer = 5000        ' pořadovaná délka animace (v ms)
        Do While x < 1
            x = Now.Subtract(casStartu).TotalMilliseconds / trvani
'TADY ZAČÍNÁ VLASTNÍ FUNKCIONALITA:
    ' buffer vyčistí a vyplní zvolenou barvou
    form_Buffer.Graphics.Clear(Color.Aqua)
    ' tady můžeme nakreslit cokoliv, jako bychom to kreslili přímo na Graphics:
    form_Buffer.Graphics.DrawString("ahoj", New Font("arial", 20), Brushes.Black, CInt(Me.DisplayRectangle.Width * (1 - x)), CInt(Me.DisplayRectangle.Height * x))
    ' a pokračujeme s kreslením....    
    form_Buffer.Graphics.DrawPie(Pens.Blue, CInt(Me.DisplayRectangle.Width * x), CInt(Me.DisplayRectangle.Height * x), 10, 10, 0, 360)
    ' a můžeme nakreslit cokoliv dále
    ' ...
    ' ...   
    ' a teprve tady se nám to najednou vyplivne do našeho formuláře
    form_Buffer.Render()
    Application.DoEvents()
        Loop
    End Sub
End Class

je to dost podobné double-bufferingu, který jste s námi dělal v rámci seriálu o directDraw, jenom je to přímo GDI+, takže se tady používají běžné GDI+ metody

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

Děkuji za odpovědi.

Na jakém principu pracuje DoubleBuffered? Myslím, kdy (podle čeho) střídá buffery?

Jak by nastavení Me.DoubleBuffered = True pomohlo ve zde probíraném případu?

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

Evidentně těch způsobů bufferingů je více a taky v tom ztrácím přehled. Možná by to některý ze zasvěcených pánů mohl vzít jako námět na článek a popsat jednotlivé možnosti a vzájemné vztahy. (v různých fázích jsem četl fůru příspěvků na toto téma, ale přiznám se, nějak se mi už plete co kam zařadit, co bylo otázkou pouhého GDI+, co už se týkalo DirectDraw, atd...

Myslím si, že jakás takás práce s grafickými výstupy různého stupně náročnosti je dost častým fenoménem dnešní doby (který uživatel dnes vydrží civět na statickou obrazovku), proto by takovéto téma možná bylo zajímavým odrazovým můstkem pro mnohé zdejší čtenáře, aby byli schopni určit, kterým směrem je nejlépe pro jejich aplikaci se vydat.

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

Záleží to případ od případu, děkuji za otestování. Ale je třeba si uvědomit, že sčítání i dělení jsou operace, které zaberou řádově (tisíckrát, desetitisíckrát, co já vím) času než samotné DoEvents. Pořád je rychlejší 1000x vydělit čísla, než jednou pustit DoEvents. Proto by se mělo používat s rozvahou a zkusit podmínku nastavit tak, aby zpomalovalo nejméně, ale aby se aplikace ještě neškubala. Opravdu záleží na tom, jakou operaci provádíte.

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

Kedysi som vo VBA takuto vec riesil cez Timer.

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.
  • 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