Triky s TreeView v .NET Compact Framework

článek je psaný a validní pro .NET Compact Framework (CF) v 2.0 a jazyk vb.net

Sepsal jsem tipy pro celořádkový výběr, přidání události click, doubleclick a v neposlední řadě velmi užitečné barvení řádků/pozadí.

Trik pro černé pozadí a bílý text

Tento problém má jednoduchý základ. Pokud vytvoříte objekt TreeView v CF není podporována vlastnost .BackColor. Samotné VS studio vás na to upozorní. Tím pádem není možné měnit barvu pozadí. První možností je[MORE] napsat vlastní TreeView odvozený z Object. Druhé řešení jsem zvolil já.

Každý přidávaný TreeNode má povoleny vlastnosti pozadí a barvy fontu. Proto je potřeba vytvořit a nastavit asi takhle:
Dim node As New TreeNode
node.Text = "aa"
node.ForeColor = Color.WhiteSmoke
node.BackColor = Color.Black
TreeView.Nodes.Add(node)

Tím máme černé pozadí.

Problémy

Černá barva je jen pod textem samotným. V CF není vlastnost pro nastavení FullRow. To lze obejít přidávání hodně (hodně hodně) mezer za zobrazený text. Něco jako +” “. Prasečina, ale funguje. Potřebujeme výsledky. Výkonostně to není problém. Vzniká tím druhotný problém s horizontálním posuvníkem. Ten nelze vypnout a tak je možnost ho buď skrýt jinou komponentou (mimo form) nebo napsat inteligentní algoritmus, který řádek doplní jen určitým potřebným množstvím mezer. Já volím první.

A poslední problém, který je potřeba vyřešit pro černé pozadí? I když bude TreeView prázdný musí být vyplněn položkami a to přesně počtem, které se vejdou na jeho výšku. Výška musí být násobek výšky řádku. Prázdné položky budou dělat černo a když začnou přibývat data, lze prázdné odebírat. Samotný algoritmus ať si každý udělá jak potřebuje.

Trik pro přidání OnClick a DoubleClickeventu(události)

TreeView nemá událost OnClick a občas by se mohla hodit :). Zase máme víc možností.

Buď se použije nízkoúrovňové GetFocus (viz msdn) nebo se použije nějaká existující událost a obalí se logikou. Na to je vhodná událost AfterSelect. Za běhu přidáme pomocí AddHandler TreeView.AfterSelect, AddressOf MojeFunkceClick. Samotná MojeFunkceClick vypadá asi takhle Sub MojeFunkceClick(ByVal sender As Object, ByVal e As System.Windows.Forms.TreeViewEventArgs). Když tedy někdo označí položku v PDA spadne to do týhle funkce. Zde máme k dispozici e proměnou, v které je například e.Node.Tag nebo e.Node.Text. Můžeme napsat logiku na ignorování našich černých prázdných řádků. Taky sem můžeme vyrobit funkce na mazání řádku atd. Ovšem aby mohl AfterSelect opět fungovat musíme TreeView.SeletedNode = nothing. Tím jsme vyrobili onclick. DoubleClick bude vyžadovat nějakou proměnou a timeout abychom mohli realizovat ten dvojklik. Nechám to na každém.

FullRow a barevně označený řádek

S předchozí funkcí TreeView.SelectedNode= nothing přijdete o označený řádek. Proto je vhodné, uchovat si který že to byl řádek kliknut a při vykreslování položek do TreeView změnit barvu pozadí/textu a tak dále. Dá se s tím docela kouzlit. Dělat multiselect a podobně. A ten FullRow. V dokumentaci je samotná funkce jen pro velký framework a ještě s jistými omezeními. Jak jsem psal výše, stačí k textu přilepit hodně mezer a je vystaráno.

Optimalizace v PDA (Compact Framework)

Za oknem léto. To každej musel zaregistrovat. Není tudíž mnoho času rozšiřovat maxigeniální myšlenky. Radši jezdím, cvičím, jím, spím a tak dále. Teď zpět k dnešnímu tématu. Několikátý měsíc, čistě pracovně, rozšiřuju svoje vědomosti o taje a zákoutí .NETího Compact Frameworku pro PocketPC alias PDA, MDA. O víkendu jsem vyvíjel novou knihovnu pro asynchronní připojení. Ta minulá byla trošku zrezlá. A když bylo dopsáno dostal jsem naprosto geniální nápad.

Vlastní dialog

Ano. Nápad byl vcelku prostý. Do skinovací knihovny přidám podporu pro vlastní dialogové okno. Samozřejmě zcela grafické. Znáte dnešní standard, na něco kliknete, celá obrazovka ztmavne a vyskočí dialog s nějakou fajnovou hláškou. Po odkliknutí to zase všechno odtmavne a jede se dál.

Úkol snadný, ne ovšem na velmi omezené platformě PDA. Tady neexistuje poloprůhlednost. Tady plno věcí neexistuje. První co jsem tedy vymyslel, byla jednoduchá FAKE aplikace, která měla předem připravené obrázky ztmaveného pozadí. Toto řešení má ovšem zásadní problém. Neumožňuje být jakkoliv flexibilní. A taky, pokud budu dialog volat odjinud, než jsem si předkreslil, nebude grafika souhlasit.

Řešení

Po nějakém tom pátrání v dokumentaci jsem pomocí PInvoke sesmolil funkci, která: sejme aktuální obrazovku PDA, uloží do bitmapy. Bitmapu potom pixel po pixelu přepočítá na “tmavé barvy” (prakticky snižuje saturaci a jas) a na tuto bitmapu doprostřed nakreslí obrázek již samotného dialogu – nějaký fancy rámeček s dialogem. Myšlenka geniální, po splácnutí preview dokonce i funkční, no bohužel smrtelně pomalá. Moje představa byla tuto operaci stihnout tak do 50-100ms. Jak dalece jsem se mýlil.


[MORE]

Optimalizace začíná

Nebudu to zdržovat. Celý proces trval neuvěřitelných 10 sekund. Za všechno mohl kód bitmap.GetPixel(x,y). A k němu reverzní SetPixel. Tyto neskutečně neoptimalizované funkce není radno používat. To už jsem věděl z dob programování grafických prvků v C++.

Zapátral jsem a od MVP lidí z MSDN jsem se dozvěděl optimalizační tipy. Základem je použít Bitmap.LockBits a UnlockBits. Tím se dostaneme přímo k nemanagované paměti bitmapy a můžeme začít řádit jak je libo. Výstupem je bajtové pole jednotlivých subpixelů(R,G,B). Pro rozlišení 240×320 je velikost pole nějakých 240*320*3=230400 pixelů. Toliko bodů je třeba nějakým způsobem zanalizovat a přepočítat (jas, kontrast, barevnost atd).

Bláhově jsem říkal, máš vyhráno. Celý proces se zrychlil na neuvěřitelných 180ms. Stačilo tedy napsat funkce na transformaci pixelu a bylo. Napsal jsem tedy vlastní RGB<->HSB transformační funkce. Sem se s nima patlal víc jak den, přetížené, patřičně okomentované, prvotřídní práce. V tuhle dobu jsem ještě netušil, že PDA je potřeba optimalizovat. Když jsem ověřil, že funkce fungují korektně, prostě jsem do smyčky s pixely vložil něco takového

ColorTransform.RGBToHSB(R, G, B, dH, dS, dB)
dB = dB * 0.4
dS = dS * 0.7
ColorTransform.HSBToRGB(dH, dS, dB, R, G, B)

Kód fungoval perfektně, výstupem bylo přesně to co jsem požadoval. Ale ouha. Transformace a násobení každého pixelu sežralo neuvěřitelných 5150ms. A tak jsem začal hledat, proč tomu tak je. Zakomentoval jsem tu část s násobením a hnedka jsem byl o pár sekund rychlejší. Už bylo jasno. Procesor v PDA stěží sčítá, natož násobí 32bit float číslo. A tak jsem přemýšlel dál.

Různá řešení

  • převést pixel na šedou a rozkopírovat do všech kanálů
  • vzít jeden kanál, dělením ztmavit a rozkopírovat do ostatních
  • sečíst všechny kanály a výsledek vydělit nějakým větším číslem
  • počítat jen sudé řádky/sloupce a výsledek rozkopírovat
  • počítat jen nutnou oblast, ostatní vynechat

Každý z těchto nápadů o něco a některé i slušně zrychlily vykonávání, ale stále jsem se pohyboval nad hranicí jedné sekundy a to bylo nepřijatelné. Musí se to počítat v reálném čase. Především výsledky s jedním kanálem vypadaly nadprůměrně hnusně. I to bych překousnul, jenže časy neodpovídaly.

No a tak jsem do noci seděl nad problémem, pořád si prohlížel hodnoty pixelu před ztmavením a po ztmavení a přemýšlel, jak tuhle hodnotu spočítat bez násobení, dělení, sčítaní. Jo a pak mě to samozřejmě trklo. Bitový posun. Není to přesné, nemá to moc možností, ale funguje to pekelně rychle kdekoliv. Tím jsem byl v podstatě hotov. Vzal jsem součet barev jednoho pixelu, shiftnul o 4 a rozkopíroval. 280ms. Super. Výstup cca dobrý.

R = (R + G + B)
R = R >> 4
G = R
B = R

Pak jsem zkusil ještě jednu úpravu a kupodivu, zrychlení a zlepšení výstupu. 255ms

R = R >> 3
G = G >> 3
B = B >> 3

Chybné cesty

Bitový posuv je tak rychlá operace, že i uvažované vynechání (nepočitání) určité oblasti, je pomalejší, než vše nechat přetáhnout posuven. Samotná režie na vyhodnocení, jestli pixel je v oblasti nebo není, je ohromná. To jsem dokonce procházení pole přepsal s doporučenými Int32 jakožto nejrychlejší možný datový typ. Počítání X a Y se samozřejmě musí provádět už v samotném cyklu nikoliv až pomocným výpočtem uvnitř cyklu. Ta režie je tak veliká, že ani vynechání všech pixelů nestačí na kompenzaci zpomalení. Tady by museli přijít taky nějaký optimalizace. Například rozdělit bitmapu pomocí BitBlt na více menším a ty zpracovávat. Nebo vyhodnocovat pozici pixelu jen občas :-/

Přikládám nějaký obrázky, jako ukázku, co je možné a za jakou dobu získat optimalizací. Rozhodně je při takových masových akcích dobře přemýšlet co a jak. Vyplatí se to. V mém případě nejde o přesné zobrazení, oko je blbý, takže je to v pohodě. Nedovedu si představit rychlost při tranformacích s maticí. Tady prostě neplatí běžná pravidla. A teď jsem dostal nápad na parádní prohlížeč obrázků :)

Reálné nasazení

Po doplácání algoritmu do mojí grafické knihovny, jsem vyzkoušel některý scénáře použití a ukázalo se to neskutečně nepoužitelný. Načtení dat z XML, vytvoření prvků, screenshot, transformace a vykreslení, to všechno zabírá víc jak sekundu času. A to je na kliknutí velká prodleva. Nakonec jsem to vyřešil vtlačením vrstvy mezi objekty. Tím se starám o cachování a preload dat. Ještě to bude chtít nějakou inteligenci pro preload. Takhle všechno zabírá moc ramky.