Použití dlaždicové 2D textury v XAML

Při portování hry Mission game z Windows Phone na Windows 8 jsem přidal obrazovku (stránku) s přehledem výsledků Score za jednotlivé mise, vytvořenou v XAMLu. Pro obrázky s náhledy misí jsem přitom použil stávající obrázek (Texture2D načtený z XNB) z MonoGame (původně XNA) části nacházející se v jiném projektu. Ten přitom, dle zásad textur pro tyto typy projektů, obsahoval náhledy všech 25ti misí v jednom obrázku. Jak tedy převést texturu do obrázku vhodného pro XAML a ještě z ní pro každý level vyříznout tu jeho část?

 

Bližší popis problému

Obrázek obsahuje náhledy misí (levelů) ve čtvercových útvarech (dlaždicích), kterých tam je 5x5. Aby se tento PNG obrázek mohl načítat v MonoGame projektu klasickým způsobem, obvykle se překompiluje do souboru s koncovkou XNB a načítá následujícím příkazem.

Texture2D missions = Content.Load<Texture2D>("pics\\missions");

Obrázky lze sice také přímo načítat z PNG souborů přidaných do projektu jako obsah (content), z něhož by pak mohl čerpat přímo i XAML, ale to by vyřešilo pouze první část problému a také, když už to bylo z verze pro mobily hotové tímto způsobem, proč to předělávat. 

Následující obrázek podrobně ilustruje celou situaci. Vlevo je celý PNG obrázek (zkompilovaný do XNB souboru). Ten je projektem načten jako Texture2D a jeho příslušná část zobrazena v přehledu misí jako náhled zvolené mise (vpravo nahoře). V tomto případě se výřez pro vykreslení definuje obdélníkem (Rectangle) předaným metodě pro vykreslení SpriteBatch.Draw. Cílem pak bylo z téhož již načteného zdroje čerpat obrázky také pro novou XAML obrazovku (vpravo dole) s jednak seznamem (ListView) obrázků všech misí, a také s větší verzí tohoto obrázku zobrazeným vpravo, po označení příslušné mise v seznamu. 

Ilustrační popis problému pro použití XNB jako Texture2D i jako ImageBitmap

Řešení

Pro účely zobrazení náhledu jedné mise byla vytvořena samostatná XAML komponenta (UserControl), která obsahovala pouze jeden obdélník (recImage), na jehož pozadí (Fill) má být tento náhled vykreslován. 

<UserControl ...>
    <Grid>
        <Rectangle x:Name="recImage" />
    </Grid>
</UserControl>

Vše ostatní se pak odehrává v C# části této komponenty.

 

Převod Texture2D na ImageBitmap

Prvním úkolem bylo převést obrázek načtený do proměnné typu Texture2D, se kterým pracuje MonoGame (ve stylu starého dobrého XNA), do ImageBitmap, se kterým zase pracuje XAML. Aby tento převod nezdržoval spuštění projektu a vytvořená mutace obrázku nezabírala místo v paměti i kdyby se třeba uživatel na obrazovku s přehledem výsledků nikdy nepodíval, proběhne tento převod až těsně před prvním vstupem do této obrazovky (ještě před jejím vytvořením). Načtený obrázek se pro jednoduchost může uložit do statické proměnné, aby byl XAML komponentě, která s ním pracuje, k dispozici při každém jejím použití (pro každý z levelů v seznamu). Společně s obrázkem se také vypočtou a uloží rozměry (šířka a výška) pro jednu dlaždici (výřez z obrázku s náhledem jedné mise), které mají všechny stejný čtvercový rozměr.

private static BitmapImage missions;
private static int imageWidth, imageHeight; 

Následující statická metoda je tedy volána vždy když se má zobrazit (vytvořit) obrazovka s přehledem výsledků. To, aby k načtení obrázku došlo pouze 1x poprvé, už si metoda ohlídá sama.

public static void CreateImage(Texture2D Image)
{
    if (Image == null || ImageBitmap != null)   // Ověření, že obrázek již nebyl načten a jeho zdroj existuje
        return;
    imageWidth = Image.Width / 5;               // Výpočet šířky jedné dlaždice
    imageHeight = Image.Height / 5;             // Výpočet výšky jedné dlaždice
    using (MemoryStream ImageStream = new MemoryStream())
    {
        Image.SaveAsPng(ImageStream, Image.Width, Image.Height);
        ImageStream.Position = 0;
        using (InMemoryRandomAccessStream ras = new InMemoryRandomAccessStream())
        {
            ImageStream.CopyTo(ras.AsStream());
            ras.Seek(0);
            BitmapImage bi = new BitmapImage();
            bi.SetSource(ras);
            missions = bi;
        }
    }
}  

 

Zobrazení pouze konkrétního výřezu obrázku

Další pomocná metoda, tentokrát již nestatická, vytvoří a nastaví výplň pro obdélník recImage, který je jediným vizuálním prvkem této komponenty. Tuto výplň přitom pomocí transformace posunu (TranslateTransform) posune o zadaný počet pixelů tak, aby levý horní roh dlaždice začínal přesně v levém horním rohu obdélníka. Druhou transformací měřítka (ScaleTransform) pak obrázek zmenší či zvětší tak, aby šířka a výška dlaždice přesně odpovídala šířce a výšce obdélníka pro vykreslení. Tyto rozměry jsou přitom komponentě definovány "zvenčí", až při jejím použití v nadřazeném kontejneru.

private void LoadImage(int posunX, int posunY)
{
    var ib = new ImageBrush();
    ib.Stretch = Stretch.None;
    ib.AlignmentX = AlignmentX.Left;
    ib.AlignmentY = AlignmentY.Top;
    ib.ImageSource = missions;
    var tg = new TransformGroup();
    var tt = new TranslateTransform();
    tt.X = -posunX;
    tt.Y = -posunY;
    tg.Children.Add(tt);
    var ts = new ScaleTransform();
    ts.ScaleX = Width / (double)imageWidth;
    ts.ScaleY = Height / (double)imageHeight;
    tg.Children.Add(ts);
    ib.Transform = tg;
    recImage.Fill = ib;
}

Díky tomuto automatickému přizpůsobování rozměru obrázku pak lze tuto komponentu použít jak v seznamu misí (vlevo), tak i pro zobrazení většího náhledu mise (vpravo). 

 

Předání informace o tom, který výřez zobrazit

Nyní již jen zbývá volat předchozí metodu pro vykreslení dlaždice se správnými parametry (posunem), podle toho, která dlaždice (mise) má být vykreslena. Jelikož je komponenta umístěna v ListView, kde se definuje pouze obecná šablona (DataTemplate) pro zobrazení položek a konkrétní hodnoty, včetně pořadového čísla mise, jsou dodány až v generickém seznamu (List<Mise>), bylo zapotřebí dodržet standardní vázací (binding) postupy. Za tímto účelem byla vytvořena plnohodnotná vlastnost LevelNumber (číslo mise 1-25), která při změně (nastavení) své hodnoty spustí metodu LevelNumberChanged.

public int LevelNumber
{
    get { return (int)GetValue(LevelNumberProperty); }
    set { SetValue(LevelNumberProperty, value); }
}

public static readonly DependencyProperty LevelNumberProperty =
    DependencyProperty.Register("LevelNumber", typeof(int), typeof(LevelImage),
    new PropertyMetadata(0, new PropertyChangedCallback(OnLevelNumberChanged)));

private static void OnLevelNumberChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    ((LevelImage)d).LevelNumberChanged(e);
}

private void LevelNumberChanged(DependencyPropertyChangedEventArgs e)
{
    int no = ((int)e.NewValue - 1);
    int x = no % 5;
    int y = no / 5;
    LoadImage(imageWidth*x, imageHeight*y);
}

Metoda LevelNumberChanged z čísla mise vypočítá horizontální (x) a vertikální (y) posun v obrázku, aby se tento nasunul k příslušné dlaždici, a zavolá metodu LoadImage, která tuto dlaždici vykreslí do obdélníka (recImage).

 

on 29 leden 2014