HaskellとWin32APIでウィンドウを表示する
GHCupでHaskell開発環境をインストール
Windows環境にGHCupでHaskell環境をインストールする。
GHCupの公式ページにあるPowerShell用インストールスクリプトを実行する。
Set-ExecutionPolicy Bypass -Scope Process -Force;[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; try { & ([ScriptBlock]::Create((Invoke-WebRequest https://www.haskell.org/ghcup/sh/bootstrap-haskell.ps1 -UseBasicParsing))) -Interactive -DisableCurl } catch { Write-Error $_ }
PS C:\Users\kazuk> Set-ExecutionPolicy Bypass -Scope Process -Force;[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; try { & ([ScriptBlock]::Create((Invoke-WebRequest https://www.haskell.org/ghcup/sh/bootstrap-haskell.ps1 -UseBasicParsing))) -Interactive -DisableCurl } catch { Write-Error $_ }
Picked C:\ as default Install prefix!
Welcome to Haskell!
This script can download and install the following programs:
* ghcup - The Haskell toolchain installer
* ghc - The Glasgow Haskell Compiler
* msys2 - A linux-style toolchain environment required for many operations
* cabal - The Cabal build tool for managing Haskell software
* stack - (optional) A cross-platform program for developing Haskell projects
* hls - (optional) A language server for developers to integrate with their editor/IDE
Please note that ANTIVIRUS may interfere with the installation. If you experience problems, consider
disabling it temporarily.
Where to install to (this should be a short Path, preferably a Drive like 'C:\')?
If you accept this path, binaries will be installed into 'C:\ghcup\bin' and msys2 into 'C:\ghcup\msys64'.
Press enter to accept the default [C:\]:
...
MinGWのコンソールが開いて、そっちでghcのインストールが進んでいる。
[ Info ] verifying digest of: ghc-9.6.7-x86_64-unknown-mingw32.tar.xz
[ Info ] Unpacking: ghc-9.6.7-x86_64-unknown-mingw32.tar.xz to C:/ghcup\tmp\ghcup-4cb72a514d18efa6
[ Info ] Installing GHC (this may take a while)
[ Info ] Merging file tree from "C:/ghcup\tmp\ghcup-4cb72a514d18efa6\ghc-9.6.7-x86_64-unknown-mingw32" to "C:/ghcup\ghc\9.6.7"
最後になんか失敗したみたいなエラーメッセージが出たけど、ちゃんと見る前に閉じてしまった。
ghcup tuiからインストールし直してみた。
[ Info ] verifying digest of: ghc-9.6.7-x86_64-unknown-mingw32.tar.xz
[ Info ] Unpacking: ghc-9.6.7-x86_64-unknown-mingw32.tar.xz to C:\ghcup\tmp\ghcup-c29af95e451ff456
[ Info ] Installing GHC (this may take a while)
[ Info ] Merging file tree from "C:\ghcup\tmp\ghcup-c29af95e451ff456\ghc-9.6.7-x86_64-unknown-mingw32" to "C:\ghcup\ghc\9.6.7"
[ Info ] HLS is not supported for 9.6.7 yet. To build from source, run:
[ ... ] ghcup compile hls -g 2.9.0.1 --ghc 9.6.7 --cabal-update -- --constraint="ghc-lib-parser == 9.8.5.20250214" --index-state="2025-02-14T12:50:38Z"
Success
[ Info ] downloading: https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-0.0.9.yaml as file C:\ghcup\cache\ghcup-0.0.9.yaml
> ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.6.7
できたっぽい。
FFIでWin32APIのMessageBoxA関数を呼び出す
FFIでMessageBoxA関数を呼び出す。
{-# LANGUAGE ForeignFunctionInterface #-}
import Foreign
import Foreign.C.String
import Foreign.C.Types
foreign import ccall "MessageBoxA"
c_MessageBox :: Ptr () -> CString -> CString -> CUInt -> IO CInt
main :: IO ()
main = do
title <- newCString "Haskell FFI"
msg <- newCString "Hello from Win32API!"
_ <- c_MessageBox nullPtr msg title 0
free title
free msg
> ghc messagebox.hs -luser32
> .\messagebox.exe
FFIでWin32APIのMessageBoxWを呼び出す
日本語(UTF-16)対応版。
{-# LANGUAGE ForeignFunctionInterface #-}
import Foreign
import Foreign.C.String
import Foreign.C.Types
foreign import ccall "MessageBoxW"
c_MessageBox :: Ptr () -> CWString -> CWString -> CUInt -> IO CInt
main :: IO ()
main = do
title <- newCWString "タイトル"
msg <- newCWString "こんにちは"
_ <- c_MessageBox nullPtr msg title 0
free title
free msg
Graphics.Win32.MiscモジュールのmessageBox関数を利用する
Haskellは標準でWin32
というパッケージを持っていて、そのパッケージのGraphics.Win32.Misc
モジュールにあるmessageBox
関数を使えば、自前でFFIを書かなくてもメッセージボックスの表示ができる。
import Graphics.Win32.Misc (mB_OK, messageBox)
main :: IO ()
main = do
messageBox Nothing "メッセージです。" "タイトル" mB_OK
return ()
とてもシンプルになった。
単純なウィンドウ表示までの手順の全体像を把握
単純なウィンドウ表示のプログラムをCで書いた場合は以下のようになる。
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE _, LPSTR _, int nCmdShow) {
WNDCLASS wc = {0};
wc.lpfnWndProc = DefWindowProc;
wc.hInstance = hInst;
wc.lpszClassName = "MyWndClass";
RegisterClass(&wc);
HWND hwnd = CreateWindow(
wc.lpszClassName,
"単純なウィンドウ",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 300, 200,
NULL, NULL, hInst, NULL
);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
手順は以下のとおり。
-
WNDCLASS
構造体のデータを用意 -
RegisterClass
関数でウィンドウクラスを登録 -
CreateWindow
関数でウィンドウを作成 -
ShowWindow
関数でウィンドウを表示 -
UpdateWindow
関数で初回の描画要求 -
GetMessage
関数でウィンドウメッセージをループで取得し続ける- 終了のウィンドウメッセージ(
WM_QUIT
)が取得されたらループを脱出する -
TranslateMessage
関数でキー入力イベントから文字入力イベントを送出(物理的なキー入力イベントのみを使用する場合は不要) -
DispatchMessage
関数で取得したウィンドウイベントをウィンドウプロシージャに送出
- 終了のウィンドウメッセージ(
WNDCLASSデータの用意
WNDCLASS
構造体は、Haskell側ではGraphics.Win32.Window
モジュールで以下のような7つの要素のタプル型のエイリアス型として定義されている。
type WNDCLASS =
(ClassStyle, -- style
HINSTANCE, -- hInstance
Maybe HICON, -- hIcon
Maybe HCURSOR, -- hCursor
Maybe HBRUSH, -- hbrBackground
Maybe LPCTSTR, -- lpszMenuName
ClassName) -- lpszClassName
任意の項目はMaybe
になっているので、省略する場合はNothing
を渡す。
インスタンスハンドルの取得
HaskellでWinMain
関数は定義できないので、インスタンスハンドルを取得するには別の手段としてgetModuleHandle
関数を使う。
getModuleHandle
関数はSystem.Win32.DLL
モジュールで定義されている。
getModuleHandle :: Maybe String -> IO HMODULE
第1引数はモジュール名(実行ファイル名)を指定するが、Nothing
を指定したら現在のプロセスの実行ファイルのハンドルが返される。
HMODULE
は
type HMODULE = Ptr ()
で、Ptr
はFFI用のForeign.Ptr
モジュールで定義されている型構築子。
試しに、getModuleHandle
で取得したモジュールハンドルの値を表示するだけのプログラムの実行を試してみる。
import System.Win32.DLL (getModuleHandle)
main :: IO ()
main = do
hModule <- getModuleHandle Nothing
print hModule
0x00007ff790150000
値は環境、状況によって異なるけど、値が取得できた。
WNDCLASS
に渡すインスタンスハンドルの型がHINSTANCE
で、getModuleHandle
で得られる値の型がHMODULE
だけど、どちらもPtr ()
のエイリアスなので問題ない。
type HINSTANCE = Ptr ()
type HMODULE = Ptr ()
ClassStyle
ClassStyle
はUINT
のエイリアスで、ウィンドウの挙動に関する指定がビットフラグによって可能だけど、今回は省略するので0
を指定する。
ClassName
クラス名はCreateWindow
するときのウィンドウクラスの識別子で、とりあえず適用な名前で良いが、こちらの型はClassName
となっている。
ClassName
の型の定義は
type ClassName = LPCTSTR
となっていて、
type LPCTSTR = LPTSTR
type LPTSTR = Ptr TCHAR
type TCHAR = CWchar
CWchar
はForeign.C.Types
の型で、これ以上は追わないが、Graphics.Win32.Window
にあるmkClassName
関数を使えばString
からClassName
を作ることができる。
mkClassName :: String -> ClassName
WNDCLASS
結果、今回のWNDCLASS
の値は以下のようにする。
hModule <- getModuleHandle Nothing
let wndClass =
( 0,
hModule,
Nothing,
Nothing,
Nothing,
Nothing,
mkClassName "SimpleWindow"
)
ウィンドウクラスの登録
RegisterClass
関数は、Haskell側ではregisterClass
関数として定義されており、以下のような型として定義されている。
registerClass :: WNDCLASS -> IO (Maybe ATOM)
ATOM
という型はSystem.Win32.Types
モジュールで定義されていて、
type ATOM = WORD
type WORD = Word16
という定義になっている。Word16
はHaskell標準のData.Word
にある型。
このATOM
の値は登録したクラスを一意に識別する値ということだけど、例ではこの値を使用していない。
先に用意したWNDCLASS
の値をregisterClass
関数に渡す。
import Graphics.Win32.Window (mkClassName, registerClass)
import System.Win32.DLL (getModuleHandle)
main :: IO ()
main = do
hModule <- getModuleHandle Nothing
let wndClass =
( 0,
hModule,
Nothing,
Nothing,
Nothing,
Nothing,
mkClassName "SimpleWindow"
)
registerClass wndClass
return ()
これでウィンドウクラスの登録ができる。
登録しただけなので、何も起きずにそのままプログラムは終了する。