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>