20211126のGoに関する記事は2件です。

GoならわかるWindows API

この記事はGoアドベントカレンダーの5記事目です。 WindowsのAPIを叩いて遊んでみた記事です、あまりGoっぽくないです。 ターミナルに文字を吐く以外のことをやってみたい 何らかの処理を行い、その実行結果をターミナルに出力するのはどの言語でもprint系の関数を利用すれば簡単に行う事ができます。 ターミナルに依存せず、起動するとGUIのウインドウが立ち上がるアプリケーションはいったいどのような仕組みで動いているんだろう、と疑問に思いました。 ウインドウを自力で生成する方法を探す 普通にGUIアプリケーションを作りたいときはフレームワークに頼るでしょう。 gotronやfyne、qtなど様々な選択肢があります。 しかし今回は仕組みを学ぶことに意義があります。 ウインドウが生成されるための最小限のコードはどんなコードだろう、と考えてみました。 同じアプリケーションであってもウインドウの細かいデザインはOSによって異なり、また同じOSであればアプリケーションの最大化、最小化機能などのUIは統一されています。 これはウインドウ生成の役割をOSが担当しているからです。 おそらくOS側の機能にウインドウを生成するためのシステムコールが存在するだろう、という推測と、Goにはシステムコールを呼ぶための機能が備わっているとGoならわかるシステムプログラミングに書いてある事から、多分できるだろうと考えました。(この記事のタイトルはそのオマージュです) さて、探してみたところWindows APIというものに辿り着きました。 WisdomSoftの情報も発見しました、WindowsのOSには新しくウインドウを作るためのCreateWindowというAPIがあるようです。 これらAPIはターミナルでコマンドを叩いて実行できる類の機能では無く、DLLとしてOSに搭載されている事がわかりました。 そしてGoでDLLの機能を実行するための方法がありました。 golang で型付きで DLL を呼び出す方法 mattnさんの記事ですね、強すぎる。 下記のようなイメージでウインドウを生成できるかな、というイメージが固まってきました。 main.go package main var ( dll = syscall.NewLazyDLL("欲しい機能がある.dll") proc = dll.NewProc("ウインドウを生成するAPI") ) func main() { i := int32(123) proc.Call(uintptr(unsafe.Pointer(&i))) } しかしCreateWindowAやCreateWindowExWなど似たような命令が沢山あり、引数の使い方とGoでのお作法もわかりません。 中々難しそうですね。 ここまで推測したタイミングで先駆者を発見しました。 こちらです、ありがとうnathan-osmanさん お目当てのCreateWindowはCreateWindowExWが動き、user32.dllに存在するようですね。 main.go var ( user32 = syscall.NewLazyDLL("user32.dll") pCreateWindowExW = user32.NewProc("CreateWindowExW") pDefWindowProcW = user32.NewProc("DefWindowProcW") pDestroyWindow = user32.NewProc("DestroyWindow") pDispatchMessageW = user32.NewProc("DispatchMessageW") pGetMessageW = user32.NewProc("GetMessageW") pLoadCursorW = user32.NewProc("LoadCursorW") pPostQuitMessage = user32.NewProc("PostQuitMessage") pRegisterClassExW = user32.NewProc("RegisterClassExW") pTranslateMessage = user32.NewProc("TranslateMessage") ) もしお使いのPCで上記のコードが動かない場合、また同じようなノリで別のAPIを呼んでみたいが、何が使えるかわからないは、dumpbin /exportsコマンドでインストールされているuser32.dllにどのようなAPIが実装されているかを確認できます。 参考: DLL 内の関数の識別 さて、nathan-osmanさんのコードを実際に動かしてみたところ、画像のようなウインドウが生成されました、すごい。 ほとんど白い画像で見づらいと思いますが、コードを実行すると真っ白なウインドウが立ち上がります。 ウインドウを生成するコードを読んでみる nathan-osmanさんのコードを読んでみると、OSのCreateWindow関数に渡す引数として、ウインドウの名前、クラス名等の設定などを渡しています。 Microsoftの公式ドキュメントにある通り、cやc++で動作していた世界にGoからアクセスするために、全ての引数はuintptrにキャストしています。 main.go func createWindow(className, windowName string, style uint32, x, y, width, height int32, parent, menu, instance syscall.Handle) (syscall.Handle, error) { ret, _, err := pCreateWindowExW.Call( uintptr(0), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(className))), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(windowName))), uintptr(style), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(parent), uintptr(menu), uintptr(instance), uintptr(0), ) if ret == 0 { return 0, err } return syscall.Handle(ret), nil } 引数のinstanceは、どの処理においてこの命令を実行するのかのコンテキストに相当するsyscall.Handleのようです。 このような下準備が必要で難しいですね。 何もしない真っ白なウインドウを一個作るだけの処理で250行ものコードを書くことになりました(先駆者のお陰で私はコピペするだけでしたが)。 main.go func getModuleHandle() (syscall.Handle, error) { ret, _, err := pGetModuleHandleW.Call(uintptr(0)) if ret == 0 { return 0, err } return syscall.Handle(ret), nil } ウインドウに情報を描画してみる ウインドウが出ただけでもそれなりに嬉しいですが、適当な絵や文字を表示してみたいと思いました。 文字を描画するためのTextOut関数が用意されている事がWisdomSoftの情報からわかりました。 私のPCにはTextOutWとTextOutAがgdi32.dllに実装されていました。 Goのコードで動く文字セットに対してはTextOutWが良いようです。(実際に試してみました、TextOutAをコールすると文字化けしたような出力が描画されます) 下記のコードをnathan-osmanさんのコードに追記します。 main.go var ( gdi32 = syscall.NewLazyDLL("gdi32.dll") pTextOut = gdi32.NewProc("TextOutW") ) func textOut(hwnd syscall.Handle, text string) (syscall.Handle, error) { ret, _, _ := pTextOut.Call( uintptr(hwnd), uintptr(0), uintptr(0), uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))), uintptr(len(text)), ) return syscall.Handle(ret), nil } このコードを呼ぶタイミングは、メインプロセスを登録している部分で宣言しているwcxのコールバック関数fnに挟み込むと良さそうです。 main.go fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { switch msg { case cWM_CLOSE: destroyWindow(hwnd) case cWM_DESTROY: postQuitMessage(0) default: ret := defWindowProc(hwnd, msg, wparam, lparam) return ret } return 0 } wcx := tWNDCLASSEXW{ wndProc: syscall.NewCallback(fn), instance: instance, cursor: cursor, background: cCOLOR_WINDOW + 1, className: syscall.StringToUTF16Ptr(className), } wcx.size = uint32(unsafe.Sizeof(wcx)) if _, err = registerClassEx(&wcx); err != nil { log.Println(err) return } このコールバック関数はウインドウの移動、拡大縮小など様々なイベント発生時に物凄い勢いで呼ばれています。 0.1秒に一回以上の頻度で呼ばれるので、ここにprintデバッグを仕込むとターミナルの挙動が凄い事になります。 main.go fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { switch msg { case cWM_CLOSE: destroyWindow(hwnd) case cWM_DESTROY: postQuitMessage(0) default: ret := defWindowProc(hwnd, msg, wparam, lparam) return ret } return 0 } 引数msgで、ウインドウがどのようなタイミングでコールバックを呼んでいるかを識別できます。 cWM_DESTROYは名前の通りウインドウを破棄する際のイベントでしょう。 main.go const ( cWM_DESTROY = 0x0002 cWM_CLOSE = 0x0010 ) 調査の結果WM_PAINTが0x000fとして定義されている事がわかりました。 定数に追加します。 main.go const ( cWM_DESTROY = 0x0002 cWM_CLOSE = 0x0010 cWM_PAINT = 0x000F ) これでコールバック関数にウインドウの描画タイミングの挙動を実装する事ができます。 ...がTextOutWが正常に動く使い方を見出すのに苦戦。 最終的にこれも先駆者のコードを発見しました、ありがとうCXさん。 main.go fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { switch msg { case cWM_CLOSE: destroyWindow(hwnd) case cWM_DESTROY: postQuitMessage(0) case cWM_PAINT: var ps PAINTSTRUCT hdc := beginPaint(hwnd, &ps) _, err := textOut(hdc, "Hello Window API") if err != nil { panic(err) } endPaint(hdc, &ps) return 0 default: ret := defWindowProc(hwnd, msg, wparam, lparam) return ret } return 0 } textOutだけでなくbeginPaint、endPaintが増えました。 beginPaint、endPaintはどの領域を描画するか、という情報をポインタでやり取りするので、そのための構造体も必要になります。 画面描画は、究極的には画面の1ピクセルを任意の色に変更するシステムコールの繰り返しです。 何かある度に変更が無い箇所も含めて画面に映る全てを再描画していては無駄な処理が多すぎます。 そこで、再描画を行う範囲を絞ることで処理の効率化を計ることができます。 下記のコードを追加します。 main.go type tRECT struct { Left int32 Top int32 Right int32 Bottom int32 } type tPAINTSTRUCT struct { hdc syscall.Handle fErace uint32 rcPaint tRECT fRestore uint32 fIncUpdate uint32 rgbReserved byte } const( pBeginPaint = user32.NewProc("BeginPaint") pEndPaint = user32.NewProc("EndPaint") ) func beginPaint(hwnd syscall.Handle, p *PAINTSTRUCT) syscall.Handle { ret, _, _ := pBeginPaint.Call( uintptr(hwnd), uintptr(unsafe.Pointer(p)), ) return syscall.Handle(ret) } func endPaint(hwnd syscall.Handle, p *PAINTSTRUCT) syscall.Handle { ret, _, _ := pEndPaint.Call( uintptr(hwnd), uintptr(unsafe.Pointer(p)), ) return syscall.Handle(ret) } 背景色が真っ白だと寂しいので色を適当に変えてみます。 background: cCOLOR_WINDOW + 1,だったコードを background: cCOLOR_WINDOW + 2,にしてみました。 main.go wcx := tWNDCLASSEXW{ wndProc: syscall.NewCallback(wndProc), instance: instance, cursor: cursor, background: cCOLOR_WINDOW + 2, className: syscall.StringToUTF16Ptr(className), } これで背景色がグレーになり、ウインドウに文字を出力する事ができました。 これだけの事に随分と苦労し、動いた時は感動しました。 これにてHello World完了です。 まとめ、GUIアプリケーションって凄い ここまで実装しましたが、これではWindowsでしか動かないですし、全てのWindows端末で動く保証はありません。 LinuxやMacでも動いて、文字だけでなく画像も表示したい、とか考えだすとキリが無いですね。 OSのAPIを愚直に利用する事で、普段利用しているアプリケーションフレームワークがどれだけ多くの事をやってくれるかわかりました。 是非皆さんも独自の機能を作ってみたり、linuxでもチャレンジしてみてください。 断片的なコード片のような書き方ばかりになってしまったので、全部まとめて整理したものをgithubに残します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go の image/color パッケージの color.RGBA に透明度を正しく指定する&過ちの共有

まえがき これは株式会社ピーアールオー(あったらいいな!を作ります) Advent Calendar 2021 2日目の記事です。 初日は pro_matuzaki さんの「周回遅れで試すNotion API」でした。 また本記事の内容は個人ブログの記事の再編です。 開発環境 go version go1.16.5 linux/amd64 要約 Go の image/color パッケージの color.RGBA に設定する RGB 値は、透明度を 0 ~ 1 に正規化した値を RGB 値に掛け合わせた値を用います。 つまり R: 162, G: 173, B: 5, A: 128 という色を指定したいなら下記のようにします。 color.RGBA{ R: uint8(162 * 128 / 255), G: uint8(173 * 128 / 255), B: uint8(5 * 128 / 255), A: uint8(128), } ちなみに color.RGBA を使わなければならない状況でないのなら素直に color.NRGBA を使ったほうがいいと思います。 本記事の「透明度を指定する」部分の結論は以上です。 * * * 以下は、この記事を書くにあたって経緯を書いていたら自分の過ちに気づいたので共有です。 初心者さんの参考になるかもしれません。 振り返ってみて過ちだったなと思った点を太字にしました。 経緯 先人の知恵を借りて実装開始 Go の勉強も兼ねて、コマンドラインで諸々指定すると OGP 画像を作れるツールを作ろうと考えました。 ただ私は Go 自体も完全に独学で習熟度低め、更にどの言語でも画像の生成について扱ったことがありませんでした。 何もわからない状態だったので、まず似たようなことをしている先人のコードを真似しながら実装を進めていきました。 さて文字色の指定なのですが color.Color インターフェースを実装した型を使います。インターフェースの中身は…… type Color interface { // RGBA returns the alpha-premultiplied red, green, blue and alpha values // for the color. Each value ranges within [0, 0xffff], but is represented // by a uint32 so that multiplying by a blend factor up to 0xffff will not // overflow. // // An alpha-premultiplied color component c has been scaled by alpha (a), // so has valid values 0 <= c <= a. RGBA() (r, g, b, a uint32) } (ちょっと難しそうなことが書いてあります……。) この説明文の理解は置いておき、具体的に文字色を指定する部分の参考コードを探しました。 色の指定に image.Black といった定数を指定しているサンプルが多くて、任意の色を指定するのに参考にできそうなコードをあまり探せなかったのですが、color.RGBA という構造体を使っているコードが 2 件ありました。 名前的にも合ってそうですし、私も color.RGBA を使うことにします。 仮実装で動きを確認 まず大枠の実装を進めたいので R: 162, G: 173, B: 5, A: 255 という RGBA 値を持つ色をソースコードにベタ書きで指定しました。 color.RGBA{ R: uint8(162), G: uint8(173), B: uint8(5), A: uint8(255), } このように正しい黄緑色の文字を出力することができました! ここはこのままにしておいて、任意のカラーコードから color.RGBA 構造体を作る部分の実装をします……。 … …… おかしいことに気づく 任意のカラーコードから color.RGBA 構造体を作る実装ができあがったので、文字色の指定をする部分と結合しました。 試してみると……6 桁のカラーコードを指定したときは正しそうですが、8 桁のカラーコードを指定すると出力結果が変です。 透明度が変わっていないし、そもそも色がおかしい。 問題の切り分けのために文字色指定をベタ打ちに戻し、A だけを小さくしてみました。 color.RGBA{ R: uint8(162), G: uint8(173), B: uint8(5), A: uint8(128), } すると このように全く違う青色になってしまいました。しかも透明度は変わっていません。 color.RGBA の使い方を理解していないことにようやく気づき、color.RGBA の説明を確認しました。 RGBA represents a traditional 32-bit alpha-premultiplied color, having 8 bits for each of red, green, blue and alpha. An alpha-premultiplied color component C has been scaled by alpha (A), so has valid values 0 <= C <= A. この説明は前に見ました。 (多分このとき Color インターフェースを実装したもの=color.RGBA みたいな謎の思い込みが発生していました。) 色の扱いに関する知識が乏しすぎて そもそも "alpha-premultiplied color" とは何か? "alpha-premultiplied color component C" とは? "C" というのは RGBA の内の RGB のことを言っているのか? ということがわからなかったため調べたところ、たぶん A を 0 ~ 1 で正規化した係数を RGB に掛けたのが "alpha-premultiplied color component C" なのだろうという理解を得ました。 下記のように試してみたところ color.RGBA{ R: uint8(162 * 128 / 255), G: uint8(173 * 128 / 255), B: uint8(5 * 128 / 255), A: uint8(128), } 意図した結果になりました! また color.RGBA について調べていて気付いたのですが、"non-alpha-premultiplied" な構造体は別途存在していて、color.NRGBA といいます。 なるほど……? もしかして color. NRGBA を使えば、こんな計算はしなくていいのでしょうか? でも誰も使っていませんでした。 それに確か color.RGBA でなければならなかったと思うのです……。 まあ個人用ツールだし、この実装自体も別に間違っていないはずなのでいいです! 一応躓いた点を個人ブログに記録して、めでたしめでたし。 過ちへの気づきと振り返り 当時この件をブログに書いてから時は流れ、アドベントカレンダーの記事としてリライトすることにしました。 なぜかというと、この件で躓いたという記事を探せなかったからです。 ネタは被らないにこしたことはないですが、本当に私以外の誰も躓かなかったんでしょうか……? 書きながら当時のことを思い起こし、image/color パッケージについても何度も読み返し、気づきました。 color.NRGBA を使えばいいですよね!? 素直に color.NRGBA を使っていれば躓くはずがありませんから、記事が無いのも納得です。 こうして過ちに気づいたところで振り返ってみて、良くなかったと思う点は下記です。 (1) 調査を放棄して先人の実装を真似した 私が新しいことを学習するときのマインドは「今理解できないことは、『こういう物だ』と一旦飲み込んでおこう。理解が進めば必ずわかるときが来る」という感じです。 たとえば数学だって、小学校の算数から始まって、中学校で負の数の概念が登場し、……と徐々にステップアップしていくわけです。いきなり高校レベルの数学を理解しようとしても無理です。 よって、全くの初心者として先人の実装を真似することから入ったのは間違いではないと思っています。 でも調査を放棄したのは間違いでした。 「名前が合っていそう」などという適当な理由で color.RGBA を使おうとせず、image/color パッケージについてきちんと調べていれば、color.NRGBA の存在には最初から気づけたと思います。 (2) color.NRGBA の存在に気づいてから、それが使えるかどうか確認しなかった 明確には思い出せないのですが、私は当時確かに color.RGBA しか使えないと強く思い込んでいました。 おそらく原因は記事中の太字にある通り color.RGBA を使っている例しか探せなかった Color インターフェースの関数の説明と color.RGBA の説明が同じだったことが印象に残っており、脳内で関連付けてしまった です。 正直なところ思い込みを自力で打破するのは困難なので仕方なかったかもしれません。 でも「本当に使えないのだろうか」と気にして試すことはできたと思います。 それに、色を RGBA で指定したいだけなのに乗算済みアルファなんていう専門用語の理解を強いられることについて疑うべきでした。 (3) 仕事でないからといって「動けばいい」的な思考をした 仕事で書いているわけではないからといって、「動けばいい」的思考をしてしまったことを反省しています。 動けばいいわけでも、実装が間違っていなければいいわけでもないです。 初心者ゆえに結果的に良くないコードになってしまったとしても、いつも最善のコードを書こうという気持ちで学習したいと思います。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む