OpenSiv3D(Windows)で自前のウィンドウプロシージャを使用する

# はじめに

OpenSiv3Dでウィンドウプロシージャを使用したい場面にであったのですが
裏で呼ばれているウィンドウプロシージャは隠蔽されていて、ユーザーコード側で触れないので
このあたりの対処法をメモしておきます。

OpenSiv3D自体にはそのような機能はないのでWin32APIで解決します。

# 実装

以下は最小限のコードです

```c++:Main.cpp
#include <Siv3D.hpp> // OpenSiv3D v0.4.3
#include <Siv3D/Windows.hpp>

namespace
{
    LONG_PTR g_baseProc = 0;
    LRESULT CALLBACK CustomWindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        // 自前のイベント処理をする

        // もともとOpenSiv3Dで呼ばれてたWindowProcを呼ぶ
        return CallWindowProc(reinterpret_cast<WNDPROC>(g_baseProc), hWnd, message, wParam, lParam);
    }
}

void Main()
{
    // Windowハンドル取得
    auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());

    // 自前のWindowProcに切り替え
    // 返り値は元々のWindowProc
    g_baseProc = ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(::CustomWindowProc));

    while (System::Update()) {
        // something
    }
}
```

たったこれだけです。

Win32APIの`SetWindowLongPtr`で自前のウィンドウプロシージャを設定し、
設定した自前のウィンドウプロシージャから`CallWindowProc`でもとのウィンドウプロシージャを呼び出すようにしています。

---

ちなみにOpenSiv3Dには`<Siv3D/Windows.hpp>`が用意されていたので
生の`<Windows.h>`をインクルードするよりこちらの使用を強く推奨します。
(`NOMINMAX`などをセットでやってくれています。)

# おまけ class化する

私はclassにしておきたかったのでむりやりclass化しました。

```cpp:Main.cpp
#include <Siv3D.hpp> // OpenSiv3D v0.4.3
#include "CustomWindowProc.hpp"

void Main()
{
    // 自前ウィンドウプロシージャ登録
    Windows::CustomWindowProc proc([](HWND, UINT, WPARAM, LPARAM)->s3d::Optional<LRESULT>{

        // 自前のイベント処理をする

        return s3d::none; // 特になければnoneを返す
    });

    while (System::Update()) {
        // something
    }
}
```
### 実装

<details>
<summary>CustomWindowProc.hpp</summary>
<div>

```c++:CustomWindowProc.hpp
#pragma once
#include <memory>
#include <functional>
#include <Siv3D/Optional.hpp>
#include <Siv3D/Windows.hpp>

namespace Windows
{
	/// <summary>
	/// カスタムウィンドウプロシージャ
	/// </summary>
	class CustomWindowProc
	{
	public:
		using Callback_t = std::function<s3d::Optional<LRESULT> CALLBACK(HWND, UINT, WPARAM, LPARAM)>;

		class Impl;
	public:

		/// <summary>
		/// コンストラクタ
		/// </summary>
		CustomWindowProc();

		/// <summary>
		/// コンストラクタ
		/// </summary>
		/// <param name="callback">登録するコールバック</param>
		CustomWindowProc(const Callback_t& callback);

		/// <summary>
		/// プロシージャのコールバックを登録
		/// </summary>
		/// <param name="callback"></param>
		void setCallback(const Callback_t& callback);
	private:
		std::shared_ptr<Impl> m_pImpl;
	};
}
```
</div>
</details>

<details>
<summary>CustomWindowProc.cpp</summary>
<div>

```c++:CustomWindowProc.cpp
#include "CustomWindowProc.hpp"
#include <Siv3D.hpp>

namespace
{
	HWND g_hWnd;
	LONG_PTR g_baseProc = 0;

	s3d::HashTable<Windows::CustomWindowProc::Impl*, Windows::CustomWindowProc::Callback_t> g_callbacks;

	LRESULT CALLBACK CustomWindowProcImpl(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
	{
		for (auto&& [_, callback] : g_callbacks) {
			if (!callback) {
				continue;
			}
			if (auto result = callback(hWnd, message, wParam, lParam); result) {
				return result.value();
			}
		}
		return CallWindowProc(reinterpret_cast<WNDPROC>(g_baseProc), hWnd, message, wParam, lParam);
	}
}

namespace Windows
{
	class CustomWindowProc::Impl
	{
	public:
		Impl()
		{
			if (g_callbacks.empty() && !g_baseProc) {
				g_hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());
				g_baseProc = ::SetWindowLongPtr(g_hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(::CustomWindowProcImpl));
			}
			g_callbacks[this] = nullptr;
		}

		~Impl()
		{
			g_callbacks.erase(this);
			if (g_callbacks.empty() && g_baseProc) {
				::SetWindowLongPtr(g_hWnd, GWLP_WNDPROC, g_baseProc);
			}
		}
	};
	CustomWindowProc::CustomWindowProc() :
		m_pImpl(std::make_shared<Impl>())
	{}

	CustomWindowProc::CustomWindowProc(const Callback_t& callback) :
		CustomWindowProc()
	{
		this->setCallback(callback);
	}

	void CustomWindowProc::setCallback(const Callback_t& callback)
	{
		g_callbacks[m_pImpl.get()] = callback;
	}
}
```
</div>
</details>

# まとめ

OpenSiv3DでWinAPI絡みののイベントハンドリングをしたい場合は

1. `SetWindowLongPtr`で自前のウィンドウプロシージャを設定
1. 自前のウィンドウプロシージャから`CallWindowProc`でもとのウィンドウプロシージャを呼び出す

で実現できました。

## 次回予告

OpenSiv3D(Windows)でウィンドウメニューを作ろう！