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
.
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:
„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ę.
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:
- W konstruktorze najpierw zapamiętujemy referencję do
Form
– przyda się. - Przy konstruowaniu obiektu
GraphicsDeviceManager
dobieramy się do jego właściwościPreferredBackBufferWidth
iPreferredBackBufferHeight
– dzięki temu rysowany obraz nie będzie zniekształcony przez niestandardowy rozmiarPictureBox
‚a. - Obsługujemy zdarzenie
PreparingDeviceSettings
i (fanfary) każemy rysować w oknie będącymPictureBox
’em (/fanfary). Parafrazując dziecko ze Shreka 3D wołamy: „No, rysuj, no!” - 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:
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:
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.
Odpowiedzi: 7 Zostaw komentarz
Hej, fajny wpis – zdecydowanie czytelniejszy niż angielsko-języczne artykuły na ten temat!
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
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.
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ą 🙂
Dzięki, bardzo przydatne, a szukałem tego już długo.
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.
Fajny wpis. Czekam na kolejne wpisy nt. XNA. Ciągle temat mało poruszany w Polsce.