Linux で .NET C# + SDL2 を使った GUI アプリを作る方法
目次
~ Ubuntu で FreeCell 風カードゲームを表示 ~
はじめに
Linux 環境で C# を使って GUI アプリを開発したい方に向けて、SDL2 ライブラリを使ったクロスプラットフォームな開発環境構築と、FreeCell風のカード表示サンプルを紹介します。
1. 開発環境の構築
1-1. 必要なパッケージのインストール
まず、.NET 8 SDK と SDL2 の開発用ライブラリをインストールします。
# Microsoft リポジトリの登録 wget https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb -O packages-microsoft-prod.deb sudo dpkg -i packages-microsoft-prod.deb # .NET SDK のインストール sudo apt-get update sudo apt-get install dotnet-sdk-8.0 # SDL2 + 画像表示ライブラリのインストール sudo apt install libsdl2-2.0-0 libsdl2-dev sudo apt install libsdl2-image-2.0-0 libsdl2-image-dev
1-2. SDL2-CS(C#バインディング)の導入
以下のコマンドで、C#プロジェクトに SDL2-CS を追加します。
dotnet new console -o SdlApp cd SdlApp dotnet add package SDL2-CS
2. サンプル構成
以下の構成でカード表示アプリを作成します。
- カード画像:
cards/C-01.png
~S-13.png
- 背景色:暗緑色
- 8列 × 最大6枚のカードを表示
3. プログラム構成
Program.cs
class Program { static void Main(string[] args) { new CardWindow().Run(); } }
CardWindow.cs
using System; using System.Collections.Generic; using SDL2; public class CardWindow { private IntPtr window; private IntPtr renderer; private bool running = true; private Dictionary<string, IntPtr> cardTextures = new(); public void Run() { SDL.SDL_Init(SDL.SDL_INIT_VIDEO); SDL_image.IMG_Init(SDL_image.IMG_InitFlags.IMG_INIT_PNG); window = SDL.SDL_CreateWindow("FreeCell Style", SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED, 1024, 768, SDL.SDL_WindowFlags.SDL_WINDOW_SHOWN); renderer = SDL.SDL_CreateRenderer(window, -1, 0); LoadCardImages("cards"); while (running) { while (SDL.SDL_PollEvent(out SDL.SDL_Event e) != 0) { if (e.type == SDL.SDL_EventType.SDL_QUIT) running = false; } SDL.SDL_SetRenderDrawColor(renderer, 0, 64, 0, 255); SDL.SDL_RenderClear(renderer); DrawCards(); SDL.SDL_RenderPresent(renderer); SDL.SDL_Delay(16); } Cleanup(); } private void LoadCardImages(string folder) { string[] suits = { "C", "D", "H", "S" }; for (int i = 1; i <= 13; i++) { foreach (string suit in suits) { string num = i.ToString("D2"); string key = $"{suit}-{num}"; string path = $"{folder}/{key}.png"; IntPtr surface = SDL_image.IMG_Load(path); if (surface == IntPtr.Zero) Console.WriteLine($"Failed to load {path}: {SDL.SDL_GetError()}"); IntPtr texture = SDL.SDL_CreateTextureFromSurface(renderer, surface); SDL.SDL_FreeSurface(surface); cardTextures[key] = texture; } } } private void DrawCards() { int cardW = 71, cardH = 96; int startX = 50, startY = 100; int spacingX = 90, spacingY = 30; string[] testOrder = { "C", "D", "H", "S", "C", "D", "H", "S" }; for (int col = 0; col < testOrder.Length; col++) { string suit = testOrder[col]; for (int row = 0; row < 6; row++) { string key = $"{suit}-{(row + 1).ToString("D2")}"; if (!cardTextures.TryGetValue(key, out var tex)) continue; SDL.SDL_Rect dst = new SDL.SDL_Rect { x = startX + col * spacingX, y = startY + row * spacingY, w = cardW, h = cardH }; SDL.SDL_RenderCopy(renderer, tex, IntPtr.Zero, ref dst); } } } private void Cleanup() { foreach (var tex in cardTextures.Values) SDL.SDL_DestroyTexture(tex); SDL.SDL_DestroyRenderer(renderer); SDL.SDL_DestroyWindow(window); SDL_image.IMG_Quit(); SDL.SDL_Quit(); } }
csproj
の設定(例)
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <!-- SDL2-CSのために必要 --> </PropertyGroup> <ItemGroup> <PackageReference Include="SDL2-CS" Version="2.0.0" /> </ItemGroup> </Project>
おわりに
このように、Linux 上でも C# と SDL2 を使って、GUI アプリやゲームの開発が可能です。カードの画像を差し替えれば、自作のソリティアやフリーセルも実現可能です!
実行プログラム
エントリーポイント
最初に呼び出されます
Program.cs
class Program { static void Main(string[] args) { new CardWindow().Run(); } }
Program.cs
カードクラス
カードの性質をモデル化します。
using SDL2; using System; public class Card { public string Suit { get; } public int Number { get; } public IntPtr Texture { get; private set; } public SDL.SDL_Rect DestRect { get; set; } public string Key => $"{Suit}-{Number.ToString("D2")}"; public Card(string suit, int number, IntPtr texture, int x, int y, int width, int height) { Suit = suit; Number = number; Texture = texture; DestRect = new SDL.SDL_Rect { x = x, y = y, w = width, h = height }; } public void Render(IntPtr renderer) { var rect = DestRect; SDL.SDL_RenderCopy(renderer, Texture, IntPtr.Zero, ref rect); } public bool HitTest(int mouseX, int mouseY) { return mouseX >= DestRect.x && mouseX <= DestRect.x + DestRect.w && mouseY >= DestRect.y && mouseY <= DestRect.y + DestRect.h; } public void Dispose() { if (Texture != IntPtr.Zero) { SDL.SDL_DestroyTexture(Texture); Texture = IntPtr.Zero; } } }
メインプログラム
実際に動くプログラムです
CardWindow.cs
using System; using System.Collections.Generic; using SDL2; public class CardWindow { private IntPtr window; private IntPtr renderer; private bool running = true; private List<Card> cards = new(); private List<Stack<Card>> columns = new(); private Card? selectedCard = null; public void Run() { Initialize(); LoadCards("cards"); while (running) { HandleInput(); Update(); Render(); SDL.SDL_Delay(16); } Cleanup(); } private void Initialize() { SDL.SDL_Init(SDL.SDL_INIT_VIDEO); SDL_image.IMG_Init(SDL_image.IMG_InitFlags.IMG_INIT_PNG); window = SDL.SDL_CreateWindow("FreeCell Click Move", SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED, 1024, 768, SDL.SDL_WindowFlags.SDL_WINDOW_SHOWN); renderer = SDL.SDL_CreateRenderer(window, -1, 0); } private void LoadCards(string folder) { int cardW = 71, cardH = 96; string[] suits = { "C", "D", "H", "S" }; foreach (var suit in suits) { for (int num = 1; num <= 13; num++) { string key = $"{suit}-{num.ToString("D2")}"; string path = $"{folder}/{key}.png"; IntPtr surface = SDL_image.IMG_Load(path); if (surface == IntPtr.Zero) continue; IntPtr texture = SDL.SDL_CreateTextureFromSurface(renderer, surface); SDL.SDL_FreeSurface(surface); if (texture == IntPtr.Zero) continue; cards.Add(new Card(suit, num, texture, 0, 0, cardW, cardH)); } } Random rand = new(); for (int i = 0; i < 8; i++) columns.Add(new Stack<Card>()); foreach (var card in cards) { int colIndex = rand.Next(8); columns[colIndex].Push(card); } } private void HandleInput() { while (SDL.SDL_PollEvent(out SDL.SDL_Event e) != 0) { if (e.type == SDL.SDL_EventType.SDL_QUIT) running = false; if (e.type == SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN) { int mouseX = e.button.x; int mouseY = e.button.y; for (int col = 0; col < columns.Count; col++) { if (columns[col].Count == 0) continue; var topCard = columns[col].Peek(); if (topCard.HitTest(mouseX, mouseY)) { selectedCard = columns[col].Pop(); return; } } if (selectedCard != null) { for (int col = 0; col < columns.Count; col++) { int colX = 50 + col * 90; if (mouseX >= colX && mouseX <= colX + 71) { columns[col].Push(selectedCard); selectedCard = null; return; } } } } } } private void Update() { } private void Render() { SDL.SDL_SetRenderDrawColor(renderer, 0, 64, 0, 255); SDL.SDL_RenderClear(renderer); DrawCards(); SDL.SDL_RenderPresent(renderer); } private void DrawCards() { int cardW = 71, cardH = 96; int startX = 50, startY = 100; int spacingX = 90, spacingY = 30; for (int col = 0; col < columns.Count; col++) { var stack = columns[col]; int x = startX + col * spacingX; Card[] stackArray = stack.ToArray(); Array.Reverse(stackArray); for (int row = 0; row < stackArray.Length; row++) { var card = stackArray[row]; card.DestRect = new SDL.SDL_Rect { x = x, y = startY + row * spacingY, w = cardW, h = cardH }; card.Render(renderer); } } } private void Cleanup() { foreach (var card in cards) card.Dispose(); SDL.SDL_DestroyRenderer(renderer); SDL.SDL_DestroyWindow(window); SDL_image.IMG_Quit(); SDL.SDL_Quit(); } }
プロジェクトファイル
コンパイル方法等を記述します。
SdlFreecell.csproj
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> <ItemGroup> <PackageReference Include="SDL2-CS" Version="2.0.0" /> </ItemGroup> </Project>