2026/03/03(火) [n年前の日記]
#1 [nim] Nimの勉強中
プログラミング言語Nimの勉強中。環境は Windows11 x64 25H2 + Nim 2.2.8 64bit。
◎ OpenGLが使えるかどうか確認 :
Nim で OpenGLを使いたい場合、opengl というライブラリを import すればいいようで、そのライブラリ/パッケージについては公式でサポートしているらしい。ただ、Nimに標準添付されてるわけでもないようで、別途インストールが必要っぽい? 以下でインストールできた…ような気がする。
ただ、OpenGLは描画関係の機能しか持たないので、ウインドウを生成したり、キー入力を検出したりする処理は別の何かで実現しないといけない。
一般的にOpenGLの勉強をする際は、GLUT、freeglut、GLFW あたりを使うことが多い印象だけど…。まあ、GLUT は古いし、バグも放置されたままらしいので、freeglut を使うようになるのだろうけど…。
_OpenGL Utility Toolkit - Wikipedia
_The freeglut Project :: About
_An OpenGL library | GLFW
しかしそのあたりは、別途 DLL が必要になりそうな気がする…。できれば exeファイル1つで動く何かを作りたい。
そこで今回は Windows API を利用してウインドウを生成したり、キー入力等を検出してみることにした。そういう構成なら別途DLLは不要のはず。Windowsに含まれてるから。
Nim から Windows API を利用したい場合、winim というライブラリを利用すると記述が楽になるらしい。
_khchen/winim: Windows API, COM, and CLR Module for Nim
インストールは以下。
AI(Google Gemini)に、空のウインドウを表示するだけのサンプルを提示してもらって動作確認した。
_main.nim
以下でビルド。main.exe が生成される。
以下のようなウインドウが表示された。
ウインドウの表示ができたので、OpenGLが使えるかテストした。これもAI(Google Gemini)にサンプルを提示してもらって動作確認した。
_main.nim
ビルドは以下。
三角形が回転するアニメーションができた。
CreateWindowEx() を呼んでウインドウを生成するとHWND(ウインドウハンドル)が得られるので、それを使って GetDC(hwnd) すれば、OpenGLを描画するためのデバイスコンテキストが得られるらしい。
ということで、どうやら Nim でも OpenGL は使えそうだなと…。
nimble install opengl
ただ、OpenGLは描画関係の機能しか持たないので、ウインドウを生成したり、キー入力を検出したりする処理は別の何かで実現しないといけない。
一般的にOpenGLの勉強をする際は、GLUT、freeglut、GLFW あたりを使うことが多い印象だけど…。まあ、GLUT は古いし、バグも放置されたままらしいので、freeglut を使うようになるのだろうけど…。
_OpenGL Utility Toolkit - Wikipedia
_The freeglut Project :: About
_An OpenGL library | GLFW
しかしそのあたりは、別途 DLL が必要になりそうな気がする…。できれば exeファイル1つで動く何かを作りたい。
そこで今回は Windows API を利用してウインドウを生成したり、キー入力等を検出してみることにした。そういう構成なら別途DLLは不要のはず。Windowsに含まれてるから。
Nim から Windows API を利用したい場合、winim というライブラリを利用すると記述が楽になるらしい。
_khchen/winim: Windows API, COM, and CLR Module for Nim
インストールは以下。
nimble install winim
AI(Google Gemini)に、空のウインドウを表示するだけのサンプルを提示してもらって動作確認した。
_main.nim
import winim/lean
# アプリの状態を保持する型(今回はウィンドウハンドルのみ)
type App = object
hwnd: HWND
# --- 1. ウィンドウプロシージャ(イベント処理) ---
proc windowProc(
hwnd: HWND, uMsg: UINT, wParam: WPARAM, lParam: LPARAM
): LRESULT {.stdcall.} =
case uMsg
of WM_DESTROY:
PostQuitMessage(0)
return 0
of WM_KEYDOWN:
if wParam == VK_ESCAPE:
# ESCキーで終了
PostMessage(hwnd, WM_CLOSE, 0, 0)
# DestroyWindow(hwnd)
return 0
else:
return DefWindowProc(hwnd, uMsg, wParam, lParam)
# --- 2. ウィンドウの生成メソッド ---
proc initApp(title: string, width, height: int): App =
let hInstance = GetModuleHandle(nil)
let className = "NimSimpleWindow"
var wc: WNDCLASS
wc.lpfnWndProc = windowProc
wc.hInstance = hInstance
wc.lpszClassName = className
wc.hCursor = LoadCursor(0, IDC_ARROW)
wc.hbrBackground = COLOR_WINDOW + 1
RegisterClass(addr wc)
let hwnd = CreateWindowEx(
0,
className,
title,
WS_OVERLAPPEDWINDOW or WS_VISIBLE,
CW_USEDEFAULT,
CW_USEDEFAULT,
width.int32,
height.int32,
0,
0,
hInstance,
nil,
)
# 作成したハンドルを App 型に包んで返す
result = App(hwnd: hwnd)
# --- 3. メインループメソッド ---
proc run(app: App) =
var msg: MSG
# GetMessageは WM_QUIT を受け取ると 0 を返し、ループが終了する
while GetMessage(addr msg, 0, 0, 0) != 0:
TranslateMessage(addr msg)
DispatchMessage(addr msg)
proc main() =
let myApp = initApp("Nim Win32 Application", 640, 480)
if myApp.hwnd == 0:
MessageBox(0, "Failed to create the application window.", "Fatal Error", MB_OK or MB_ICONERROR)
return
run(myApp)
main()
以下でビルド。main.exe が生成される。
nim c -d:release main.nim or nim c -d:release --app:gui main.nim
- -d:release の指定はデバッグ情報を削除してファイルサイズを小さくしたり、最適化して処理速度を向上させる指定。
- --app:gui の指定はコンソールを出さない指定。
以下のようなウインドウが表示された。
ウインドウの表示ができたので、OpenGLが使えるかテストした。これもAI(Google Gemini)にサンプルを提示してもらって動作確認した。
_main.nim
import winim/lean
import opengl
import std/monotimes
# 必要な情報をまとめる構造体
type AppWindow = object
hwnd: HWND
hdc: HDC
hrc: HGLRC
# --- 1. ウィンドウプロシージャ(イベント処理) ---
proc windowProc(
hwnd: HWND, uMsg: UINT, wParam: WPARAM, lParam: LPARAM
): LRESULT {.stdcall.} =
case uMsg
of WM_DESTROY:
PostQuitMessage(0)
return 0
of WM_KEYDOWN:
if wParam == VK_ESCAPE:
PostMessage(hwnd, WM_CLOSE, 0, 0)
return 0
else:
return DefWindowProc(hwnd, uMsg, wParam, lParam)
# --- 2. ウィンドウとOpenGLの初期化 ---
proc initWindow(width, height: int): AppWindow =
let hInstance = GetModuleHandle(nil)
let className = "NimGLWindow"
var wc: WNDCLASS
wc.lpfnWndProc = windowProc
wc.hInstance = hInstance
wc.lpszClassName = className
wc.hCursor = LoadCursor(0, IDC_ARROW)
RegisterClass(addr wc)
let hwnd = CreateWindowEx(
0,
className,
"Nim OpenGL - Single EXE Framework",
WS_OVERLAPPEDWINDOW or WS_VISIBLE,
CW_USEDEFAULT,
CW_USEDEFAULT,
width.int32,
height.int32,
0,
0,
hInstance,
nil,
)
# OpenGL初期化
let hdc = GetDC(hwnd)
var pfd: PIXELFORMATDESCRIPTOR
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR).uint16
pfd.nVersion = 1
pfd.dwFlags = PFD_DRAW_TO_WINDOW or PFD_SUPPORT_OPENGL or PFD_DOUBLEBUFFER
pfd.iPixelType = PFD_TYPE_RGBA
pfd.cColorBits = 32
let pixFormat = ChoosePixelFormat(hdc, addr pfd)
SetPixelFormat(hdc, pixFormat, addr pfd)
let hrc = wglCreateContext(hdc)
wglMakeCurrent(hdc, hrc)
loadExtensions() # モダンな関数を使えるようにロード
result = AppWindow(hwnd: hwnd, hdc: hdc, hrc: hrc)
# --- 3. 描画ロジック(アスペクト比補正付き) ---
proc render(app: AppWindow, angle: float32) =
# ウィンドウサイズの取得
var rect: RECT
GetClientRect(app.hwnd, addr rect)
let w = (rect.right - rect.left).float32
let h = (rect.bottom - rect.top).float32
# ビューポートと投影行列の設定(引き伸ばし防止)
glViewport(0, 0, w.int32, h.int32)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
let aspect =
if h != 0:
w / h
else:
1.0
if w >= h:
glOrtho(-aspect, aspect, -1.0, 1.0, -1.0, 1.0)
else:
glOrtho(-1.0, 1.0, -1.0 / aspect, 1.0 / aspect, -1.0, 1.0)
# モデルビュー行列のリセットと描画
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
glClearColor(0.1, 0.2, 0.3, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
# 回転の適用
glRotatef(angle, 0, 0, 1)
# 三角形の描画
glBegin(GL_TRIANGLES)
glColor3f(1.0, 0.0, 0.0)
glVertex2f(0.0, 0.8)
glColor3f(0.0, 1.0, 0.0)
glVertex2f(-0.8, -0.8)
glColor3f(0.0, 0.0, 1.0)
glVertex2f(0.8, -0.8)
glEnd()
# --- 4. メインループ ---
proc mainLoop(app: AppWindow) =
var msg: MSG
var angle: float32 = 0.0
let rotationSpeed: float32 = 90.0 # 1秒間に90度
var lastTicks = getMonoTime().ticks
while true:
if PeekMessage(addr msg, 0, 0, 0, PM_REMOVE) != 0:
if msg.message == WM_QUIT:
break
TranslateMessage(addr msg)
DispatchMessage(addr msg)
else:
# デルタタイム計算 (.ticks を使用)
let currentTicks = getMonoTime().ticks
let dt = (currentTicks - lastTicks).float32 / 1_000_000_000.0
lastTicks = currentTicks
angle += rotationSpeed * dt
# if angle > 360.0:
# angle -= 360.0
render(app, angle)
SwapBuffers(app.hdc)
Sleep(15)
# --- 5. メイン ---
proc main() =
let app = initWindow(800, 600)
# 使用中のOpenGLのバージョンを表示
# echo "Vendor: ", $glGetString(GL_VENDOR)
# echo "Renderer: ", $glGetString(GL_RENDERER)
# echo "Version: ", $glGetString(GL_VERSION)
mainLoop(app)
main()
ビルドは以下。
nim c -d:release main.nim or nim c -d:release --app:gui main.nim
三角形が回転するアニメーションができた。
CreateWindowEx() を呼んでウインドウを生成するとHWND(ウインドウハンドル)が得られるので、それを使って GetDC(hwnd) すれば、OpenGLを描画するためのデバイスコンテキストが得られるらしい。
ということで、どうやら Nim でも OpenGL は使えそうだなと…。
[ ツッコむ ]
以上です。

