Open8

HaskellとWin32APIでウィンドウを表示する

Kazuki MiyanishiKazuki Miyanishi

GHCupでHaskell開発環境をインストール

Windows環境にGHCupでHaskell環境をインストールする。

GHCupの公式ページにあるPowerShell用インストールスクリプトを実行する。

https://www.haskell.org/ghcup/

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

できたっぽい。

Kazuki MiyanishiKazuki Miyanishi

FFIでWin32APIのMessageBoxA関数を呼び出す

FFIでMessageBoxA関数を呼び出す。

messagebox.hs
{-# 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

Kazuki MiyanishiKazuki Miyanishi

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

Kazuki MiyanishiKazuki Miyanishi

Graphics.Win32.MiscモジュールのmessageBox関数を利用する

Haskellは標準でWin32というパッケージを持っていて、そのパッケージのGraphics.Win32.MiscモジュールにあるmessageBox関数を使えば、自前でFFIを書かなくてもメッセージボックスの表示ができる。

https://hackage-content.haskell.org/package/Win32-2.14.2.1/docs/Graphics-Win32-Misc.html

import Graphics.Win32.Misc (mB_OK, messageBox)

main :: IO ()
main = do
  messageBox Nothing "メッセージです。" "タイトル" mB_OK
  return ()

とてもシンプルになった。

Kazuki MiyanishiKazuki Miyanishi

単純なウィンドウ表示までの手順の全体像を把握

単純なウィンドウ表示のプログラムをCで書いた場合は以下のようになる。

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;
}

手順は以下のとおり。

  1. WNDCLASS構造体のデータを用意
  2. RegisterClass関数でウィンドウクラスを登録
  3. CreateWindow関数でウィンドウを作成
  4. ShowWindow関数でウィンドウを表示
  5. UpdateWindow関数で初回の描画要求
  6. GetMessage関数でウィンドウメッセージをループで取得し続ける
    1. 終了のウィンドウメッセージ(WM_QUIT)が取得されたらループを脱出する
    2. TranslateMessage関数でキー入力イベントから文字入力イベントを送出(物理的なキー入力イベントのみを使用する場合は不要)
    3. DispatchMessage関数で取得したウィンドウイベントをウィンドウプロシージャに送出

WNDCLASSデータの用意

WNDCLASS構造体は、Haskell側ではGraphics.Win32.Windowモジュールで以下のような7つの要素のタプル型のエイリアス型として定義されている。

Haskell
type WNDCLASS =
 (ClassStyle,    -- style
  HINSTANCE,     -- hInstance
  Maybe HICON,   -- hIcon
  Maybe HCURSOR, -- hCursor
  Maybe HBRUSH,  -- hbrBackground
  Maybe LPCTSTR, -- lpszMenuName
  ClassName)     -- lpszClassName

任意の項目はMaybeになっているので、省略する場合はNothingを渡す。

Kazuki MiyanishiKazuki Miyanishi

インスタンスハンドルの取得

HaskellでWinMain関数は定義できないので、インスタンスハンドルを取得するには別の手段としてgetModuleHandle関数を使う。

getModuleHandle関数はSystem.Win32.DLLモジュールで定義されている。

https://hackage-content.haskell.org/package/Win32-2.14.2.1/docs/System-Win32-DLL.html#v:getModuleHandle

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 ()
Kazuki MiyanishiKazuki Miyanishi

ClassStyle

ClassStyleUINTのエイリアスで、ウィンドウの挙動に関する指定がビットフラグによって可能だけど、今回は省略するので0を指定する。

ClassName

クラス名はCreateWindowするときのウィンドウクラスの識別子で、とりあえず適用な名前で良いが、こちらの型はClassNameとなっている。

ClassNameの型の定義は

type ClassName = LPCTSTR

となっていて、

type LPCTSTR = LPTSTR
type LPTSTR = Ptr TCHAR
type TCHAR = CWchar

CWcharForeign.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"
      )
Kazuki MiyanishiKazuki Miyanishi

ウィンドウクラスの登録

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 ()

これでウィンドウクラスの登録ができる。

登録しただけなので、何も起きずにそのままプログラムは終了する。