iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🪟

Displaying a Window with Haskell and the Win32 API

に公開

This is an article for Day 24 of the Haskell Advent Calendar 2025.

You can call the Win32 API from Haskell by using the System.Win32 module in GHC. In this article, I will try to display a simple window using this System.Win32 module.

Overview of the steps for simple window display

Before trying to display a window using System.Win32 in Haskell, let's review the flow of a program that displays a simple window written in C. The flow is as follows:

#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,
        "Simple Window",
        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;
}

The procedure is as follows:

  1. Prepare data for the WNDCLASS structure
  2. Register the window class with the RegisterClass function
  3. Create a window with the CreateWindow function
  4. Display the window with the ShowWindow function
  5. Request the initial redraw with the UpdateWindow function
  6. Continue to retrieve window messages in a loop with the GetMessage function
    1. Exit the loop when the termination window message (WM_QUIT) is received
    2. Send character input events from key input events with the TranslateMessage function (unnecessary if using only physical key input events)
    3. Send the retrieved window events to the window procedure with the DispatchMessage function

We will now replace this with a program written in Haskell using the System.Win32 package.

Preparing WNDCLASS data

On the Haskell side, the WNDCLASS structure is defined in the Graphics.Win32.Window module as a type alias for a tuple type with seven elements, as shown below:

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

Optional items are wrapped in Maybe, so you can pass Nothing when omitting them.

Getting the instance handle

Since you cannot define a WinMain function in Haskell, you must use the getModuleHandle function as an alternative way to obtain an instance handle.

The getModuleHandle function is defined in the System.Win32.DLL module.

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

getModuleHandle :: Maybe String -> IO HMODULE

The first argument specifies the module name (executable file name), but if you specify Nothing, the handle for the executable file of the current process is returned.

HMODULE is defined as:

type HMODULE = Ptr ()

where Ptr is a type constructor defined in the Foreign.Ptr module for FFI.

As a test, let's try running a program that simply displays the value of the module handle obtained with getModuleHandle.

import System.Win32.DLL (getModuleHandle)

main :: IO ()
main = do
  hModule <- getModuleHandle Nothing
  print hModule
0x00007ff790150000

The value will vary depending on the environment and situation, but we were able to obtain a value.

While the type of the instance handle passed to WNDCLASS is HINSTANCE and the type obtained from getModuleHandle is HMODULE, both are aliases for Ptr (), so there is no problem.

type HINSTANCE = Ptr ()
type HMODULE = Ptr ()

ClassStyle

ClassStyle is an alias for UINT, and you can specify window behaviors using bit flags. However, since we are omitting them this time, we will specify 0.

ClassName

The class name is an identifier for the window class used when calling CreateWindow. Any suitable name is fine for now, but its type is ClassName.

The definition of the ClassName type is as follows:

type ClassName = LPCTSTR

which goes like:

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

CWchar is a type from Foreign.C.Types. I won't go any deeper than this, but it seems that you can create a ClassName from a String using the mkClassName function in Graphics.Win32.Window.

mkClassName :: String -> ClassName

WNDCLASS

As a result, we will set the WNDCLASS value for this instance as follows:

hModule <- getModuleHandle Nothing
let wndClass =
      ( 0,
        hModule,
        Nothing,
        Nothing,
        Nothing,
        Nothing,
        mkClassName "SimpleWindow"
      )

Registering the Window Class

The RegisterClass function is defined as the registerClass function on the Haskell side, with the following type signature:

registerClass :: WNDCLASS -> IO (Maybe ATOM)

The ATOM type is defined in the System.Win32.Types module as:

type ATOM = WORD
type WORD = Word16

Word16 is a type from the standard Haskell Data.Word module.

This ATOM value is a value that uniquely identifies the registered class, but we did not use this value in the C example.

Now, pass the WNDCLASS value prepared earlier to the registerClass function.

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

This completes the registration of the window class.

Since we have only registered the window class, running this program will result in nothing happening, and the program will simply terminate.

Creating a Window

Create a window using the createWindow function from Graphics.Win32.Window.

createWindow
  :: ClassName -> String -> WindowStyle ->
     Maybe Pos -> Maybe Pos -> Maybe Pos -> Maybe Pos ->
     Maybe HWND -> Maybe HMENU -> HINSTANCE -> WindowClosure ->
     IO HWND

The last argument is WindowClosure, which does not exist in the Win32 API's CreateWindow function.

Originally, the window procedure is specified in the WNDCLASS structure. However, on the Haskell side, it seems the window procedure is specified via this createWindow call instead of WNDCLASS, and that is what WindowClosure is for.

Since the original Win32 API CreateWindow function has no argument corresponding to WindowClosure, there might be some internal reasons for why specifying a function here allows it to be called as the window procedure. For now, it's enough to know that the WindowClosure specified here will be called as the window procedure.

This WindowClosure is not a Maybe, so you must pass a function even if the application does nothing.

The type of WindowClosure is as follows:

type WindowClosure = HWND -> WindowMessage -> WPARAM -> LPARAM -> IO LRESULT

In the C sample provided earlier, DefWindowProc was specified directly as the window procedure to handle the window's standard behavior. Haskell also provides a defWindowProc function, but its first argument Maybe HWND differs from the HWND in WindowClosure.

defWindowProc :: Maybe HWND -> WindowMessage -> WPARAM -> LPARAM -> IO LRESULT

Therefore, to create a window procedure that only performs standard actions, you need to wrap HWND in Just.

windowClosure :: WindowClosure
windowClosure hWnd = defWindowProc (Just hWnd)

With this in mind, call the createWindow function.

hWnd <-
  createWindow
    className
    "Window Title"
    wS_OVERLAPPEDWINDOW
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    Nothing
    hModule
    windowClosure

Now the window itself can be created, but running this will still not display the window.

Displaying the window

To display the window, use the showWindow function.

showWindow :: HWND -> ShowWindowControl -> IO Bool

For ShowWindowControl, sW_SHOWNORMAL is sufficient for now.

showWindow hWnd sW_SHOWNORMAL

To have the drawing within the window executed immediately, call the updateWindow function.

updateWindow :: HWND -> IO ()
updateWindow hWnd

At this point, the window will be displayed, but the program will terminate immediately, and the window will close in an instant.

Message loop

To prevent the program from terminating immediately, you need to implement a message loop.

First, prepare a function for the message loop and allocate memory to receive messages.

To allocate memory for receiving messages, use the allocaMessage function. This seems to be unique to Haskell's Win32 package rather than being part of the Win32 API itself.

allocaMessage :: forall a. (LPMSG -> IO a) -> IO a

LPMSG is a pointer to the memory where message information will be stored.

messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> messageLoop

Next, retrieve the window message. Use the getMessage function to get the window message.

getMessage :: LPMSG -> Maybe HWND -> IO Bool
messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> do
  getMessage msg Nothing
  messageLoop

When a program termination message (WM_QUIT) is received as a window message, the getMessage function returns False; otherwise, it returns True. Therefore, we will ensure that the message loop continues only when True is received.

import Control.Monad (when)

messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> do
  continue <- getMessage msg Nothing
  when continue $ do
    messageLoop

Use the dispatchMessage function to send the retrieved message to the window procedure.

dispatchMessage :: LPMSG -> IO LONG
messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> do
  continue <- getMessage msg Nothing
  when continue $ do
    dispatchMessage msg
    messageLoop

While not strictly required this time, call the translateMessage function to convert key input messages into character input messages.

messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> do
  continue <- getMessage msg Nothing
  when continue $ do
    translateMessage msg
    dispatchMessage msg
    messageLoop

Calling messageLoop at the end of the main function starts the message loop. When you run this program, the window will be displayed.


A simple window is displayed

Terminating the program when the window is closed

If the window procedure remains as defWindowProc, the program will not terminate when the window is closed (when the "Close" button is clicked). Therefore, we need to ensure the program terminates when the window is closed.

The program can be terminated by exiting the message loop, but the message loop was set up to continue as long as getMessage returns True.

messageLoop = allocaMessage $ \msg -> do
  continue <- getMessage msg Nothing
  when continue $ do
    -- Omitted

By calling the postQuitMessage function, you can make getMessage return False.

postQuitMessage :: Int -> IO ()

The postQuitMessage function takes one Int argument, which represents the exit code. If there are no specific issues, 0 is fine.

postQuitMessage 0 -- Terminates the message loop

When the window is closed, a window message called wM_DESTROY is sent. We capture this message in the window procedure and call postQuitMessage there.

windowClosure hWnd msg w l
  | msg == wM_DESTROY =
      postQuitMessage 0
      return 0

The type of windowClosure is the function type WindowClosure, whose return type is IO LRESULT. Since the return type of postQuitMessage is IO (), which doesn't match, we use return 0 to provide a return value that fits the type.

Also, for window messages other than wM_DESTROY, we need to call defWindowProc for the default behavior.

windowClosure :: WindowClosure
windowClosure hWnd msg w l
  | msg == wM_DESTROY = do
      postQuitMessage 0
      return 0
  | otherwise = do
      defWindowProc (Just hWnd) msg w l

With this, we can now display a simple window. Although some parts could be refactored to better utilize monads, we have achieved the goal of displaying a window for now.

Complete program

The complete program is as follows.

import Control.Monad (when)
import Graphics.Win32 (iDC_ARROW, loadCursor)
import Graphics.Win32.Message (wM_DESTROY)
import Graphics.Win32.Window
  ( WindowClosure,
    allocaMessage,
    createWindow,
    defWindowProc,
    dispatchMessage,
    getMessage,
    mkClassName,
    registerClass,
    sW_SHOWNORMAL,
    showWindow,
    translateMessage,
    updateWindow,
    wS_OVERLAPPEDWINDOW,
  )
import Graphics.Win32.Window.PostMessage (postQuitMessage)
import System.Win32.DLL (getModuleHandle)

windowClosure :: WindowClosure
windowClosure hWnd msg w l
  | msg == wM_DESTROY = do
      postQuitMessage 0
      return 0
  | otherwise = do
      defWindowProc (Just hWnd) msg w l

messageLoop :: IO ()
messageLoop = allocaMessage $ \msg -> do
  continue <- getMessage msg Nothing
  when continue $ do
    translateMessage msg
    dispatchMessage msg
    messageLoop

main :: IO ()
main = do
  hModule <- getModuleHandle Nothing
  hCursor <- loadCursor Nothing iDC_ARROW
  let className = mkClassName "SimpleWindow"
  let wndClass =
        (0, hModule, Nothing, Just hCursor, Nothing, Nothing, className)

  registerClass wndClass

  hWnd <-
    createWindow
      className
      "Window Title"
      wS_OVERLAPPEDWINDOW
      Nothing
      Nothing
      Nothing
      Nothing
      Nothing
      Nothing
      hModule
      windowClosure

  showWindow hWnd sW_SHOWNORMAL
  updateWindow hWnd

  messageLoop

  return ()

Discussion