Osadzanie XNA 4.0 w Windows Forms

XNA 4.0 jest bardzo przyjemnym frameworkiem do programowania gier: ma bogatą bibliotekę matematyczną, kilka predefiniowanych efektów (shaderów), dzięki którym można po krótkiej chwili zobaczyć efekt swojej pracy na ekranie. Brakuje jej natomiast bardzo według mnie istotnego elementu, jakim jest biblioteka do obsługi graficznego interfejsu użytkownika (Graphical User Interface, dalej będę posługiwał się skrótem GUI). Z drugiej strony, pisząc już kilka lat aplikacje pod .NET, projektując GUI zawsze myślę w kategoriach Windows Forms – przyzwyczaiłem się, i tyle. W dalszej części wpisu przedstawię sposób na połączenie obu technologii – stworzenie „WindowsFormsowej” formy, osadzenie na niej kilku kontrolek do sterowania aplikacją (będą to zwykłe przyciski), uruchomienie na formie renderowania za pomocą XNA oraz reagowanie na akcje użytkownika wewnątrz obiektu Game.

Windows Game

Rozpoczniemy pracę od utworzenia projektu typu Windows Game 4.0.

Projekt Windows Game 4.0

Do standardowo wygenerowanego kodu dodałem listę pozycji napisów „Hello World„, które będą rysowane, inicjalizowanie jej pojedynczą wartością i wypisywanie „Hello World” dla każdej pozycji na liście. Po skompilowaniu powinno się otrzymać następujący widok:

Standardowe okno XNA

„Zaraz, zaraz” – może zakrzyknąć w tym momencie przerażony Czytelnik (przyznaj się, krzyknęłaś/krzyknąłeś?) – „przecież to standardowe okno XNA, a nie Windows Forms!” Uspokoję Cię, masz rację. W tym momencie wchodzi do gry..

Windows Forms

Dodajmy do projektu formę.

Budujemy formę ;)

Na formę wrzuciłem dwa przyciski (btnAdd oraz btnRemove) i PictureBox (canvas), na którym całe to tałatajstwo będzie rysowane. W tym momencie trochę magii: należy udostępnić tzw. uchwyty (Handle) do umieszczonych na formie kontrolek, jeżeli chcemy mieć do nich dostęp z poziomu XNA. Załatwimy to zestawem właściwości dla Formy:

public IntPtr ButtonAddHandle
{
    get { return btnAdd.Handle; }
}

public IntPtr ButtonRemoveHandle
{
    get { return btnRemove.Handle; }
}

public IntPtr CanvasHandle
{
    get { return canvas.Handle; }
}

public Size ViewportSize
{
    get { return canvas.Size; }
}

Modyfikacja obiektu Game

Obiekt Game musi wiedzieć, że rysowanie nie następuje w normalnym oknie, które sam sobie stworzył, ale w kontrolce, którą mu przekazujemy. Należy zmodyfikować konstruktor klasy Game, dla ułatwienia przekażemy do niego cały obiekt Formy, żeby mieć dostęp do wszystkich elementów. Poniżej kod klasy MainGame po tych zabiegach.

using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace XNAinWindowsForms
{
    public class MainGame : Game
    {
        private readonly GraphicsDeviceManager graphics;
        private SpriteBatch spriteBatch;
        private SpriteFont spriteFont;
        private readonly MainForm Form;
        private List<Vector2> helloWorldPositions;

        public MainGame(MainForm form)
        {
            Form = form;
            graphics = new GraphicsDeviceManager(this)
            {
                PreferredBackBufferWidth = Form.ViewportSize.Width,
                PreferredBackBufferHeight = Form.ViewportSize.Height
            };

            graphics.PreparingDeviceSettings += graphics_PreparingDeviceSettings;
            System.Windows.Forms.Control.FromHandle(Window.Handle).VisibleChanged += MainGame_VisibleChanged;
            Content.RootDirectory = "Content";
            IsMouseVisible = true;
        }

        void MainGame_VisibleChanged(object sender, System.EventArgs e)
        {
            if (System.Windows.Forms.Control.FromHandle(Window.Handle).Visible)
                System.Windows.Forms.Control.FromHandle(Window.Handle).Visible = false;
        }

        void graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
        {
            e.GraphicsDeviceInformation.PresentationParameters.DeviceWindowHandle = Form.CanvasHandle;
        }

        protected override void Initialize()
        {
            helloWorldPositions = new List<Vector2> { Vector2.Zero };
            base.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            spriteFont = Content.Load<SpriteFont>("DefaultFont");
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();

            foreach (Vector2 position in helloWorldPositions)
                spriteBatch.DrawString(spriteFont, "Hello World", position, Color.White);

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

Po kolei:

  1. W konstruktorze najpierw zapamiętujemy referencję do Form – przyda się.
  2. Przy konstruowaniu obiektu GraphicsDeviceManager dobieramy się do jego właściwości PreferredBackBufferWidth i PreferredBackBufferHeight – dzięki temu rysowany obraz nie będzie zniekształcony przez niestandardowy rozmiar PictureBox‚a.
  3. Obsługujemy zdarzenie PreparingDeviceSettings i (fanfary) każemy rysować w oknie będącym PictureBox’em (/fanfary). Parafrazując dziecko ze Shreka 3D wołamy: „No, rysuj, no!”
  4. Obsługujemy kolejne zdarzenie, tym razem ukrywając standardowe okno XNA.

Oczywiście musimy zmodyfikować też punkt wejściowy aplikacji, czyli plik Program.cs

namespace XNAinWindowsForms
{
#if WINDOWS || XBOX
    static class Program
    {
        static void Main()
        {
            MainForm mainForm = new MainForm();
            mainForm.Show();

            MainGame game = new MainGame(mainForm);
            game.Run();
        }
    }
#endif
}

Tu kod jest tak prosty, że nie będę go komentował 😉 Pozostała jeszcze jedna ważna rzecz. Gdy zamkniemy formę, aplikacja nadal działa w tle. Powinniśmy zwolnić wszystkie zasoby etc. etc. ale, że jest to kod jedynie poglądowy obsłużymy zdarzenie zamknięcia formy w ten sposób:

private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
{
    // Life is brutal
    Application.Exit();
}

Po tych zabiegach możemy nacieszyć oczy takim oto obrazkiem:

XNA w Windows Forms

Obsługa zdarzeń

Nie po to wcześniej wykonaliśmy katorżniczą pracę wstawiając dwa przyciski, żeby teraz z nich nie skorzystać. Przycisk Add będzie dodawał do listy losową pozycję do narysowania kolejnego napisu, natomiast Remove będzie z tej listy pozycje usuwał.

private void CreateEventHandlers()
{
    System.Windows.Forms.Control.FromHandle(Form.ButtonAddHandle).Click += MainGameButtonAdd_Click;
    System.Windows.Forms.Control.FromHandle(Form.ButtonRemoveHandle).Click += MainGameButtonRemove_Click;
}

void MainGameButtonAdd_Click(object sender, EventArgs e)
{
    Random r = new Random();
    helloWorldPositions.Add(new Vector2(r.Next(Form.ViewportSize.Width), r.Next(Form.ViewportSize.Height)));
}

void MainGameButtonRemove_Click(object sender, EventArgs e)
{
    if (helloWorldPositions.Count == 0)
        return;

    helloWorldPositions.RemoveAt(0);
}

Wywołanie funkcji CreateEventHandlers wrzucamy gdzieś w kod inicjalizujący. Po uruchomieniu programu i naciśnięciu parę(-naście/ -dziesiąt/ -set (jest ktoś, komu się chciało tyle klikać?)) razy otrzymamy coś takiego:

Więcej napisów

Kod źródłowy do pobrania (projekt MS Visual Studio 2010 [18.5kB])

Zostańcie zatuńczykowani! (ang. Stay tuned!)

P.S.

Ten wpis jest pierwszym w kategorii „Daj się poznać”. Mam nadzieję, że uda mi się choć częściowo zaimplementować projekt, z którym wystartowałem. Funkcjonalność opisana powyżej na pewno pomoże mi we wstępnym prototypowaniu.

Be Sociable, Share!
czoper opublikowano dnia 2010-9-8 Kategoria: Daj się poznać, Programowanie, Theme Festival | Tagi:, , , ,

Odpowiedzi: 7 Zostaw komentarz

  1. #7Alek @ 2015-10-29 16:36

    Hej, fajny wpis – zdecydowanie czytelniejszy niż angielsko-języczne artykuły na ten temat!

  2. #6Majkel @ 2012-12-30 18:12

    Bardzo fajny artykuł, jednak mam pytanie jak mogę zgrać kursor myszki w xna z kursorem myszy winforms, ponieważ mam problem z przesunięciem myszy(kilka pikseli) w okienku xna osadzonym w winforms

  3. #5dom @ 2011-9-10 10:22

    temat jest głęboki. trzeba oprócz tego co napisane dopisać odbieranie danych o mouse-scrollu, no i performance spada i tak =/. podobno poprawia sie w fullscreenie ale nie u mnie.

  4. #4Emil @ 2010-12-11 12:51

    Proste rozwiązanie problemu jednak jest mały problem.
    Stworzyłem najpierw mały app w XNA pokazujący położenie myszy oraz prosty button. Położenie wskazywało prawidłowo względem ekranu XNA. Po dodaniu Forma położenie kursora ‚zwariowało’. Z:

    mouseState = Mouse.GetState();
    a = „X: ” + mouseState.X + ” Y: ” + mouseState.Y; //string

    otrzymywałem dziwne dane. Wygląda to tak jakby okno w XNA stało w miejscu, natomiast obraz w kontrolce się poruszał. Szukam sposobu aby to ominąć:P

    Pomimo tego gratuluję wiedzy i dziękuję za podzielenie się nią 🙂

  5. #3Paweł Miziołek @ 2010-11-21 10:07

    Dzięki, bardzo przydatne, a szukałem tego już długo.

  6. #2Mikołaj Ślefarski @ 2010-11-5 16:00

    Bardzo dziękuję za ten wpis- opisałeś w prosty i przystępny sposób to, czego szukałem. „Keep up the good work!”

    Pozdrawiam.

  7. #1Dawid Pośliński @ 2010-9-9 17:32

    Fajny wpis. Czekam na kolejne wpisy nt. XNA. Ciągle temat mało poruszany w Polsce.

Zostaw odpowiedź

(Ctrl + Enter)