Windows APIを使って、ペイントに「大石泉すき」と描かせる

この記事は[「大石泉すき」アドベントカレンダー](https://qiita.com/advent-calendar/2019/ohishi-izumi-suki) 20日目の記事となります。
Windows APIを使って何かちょっと動きのあるプログラムを作りたいと思い、このようなテーマにしました。

#概要
適当にテキストボックスとボタンを配置した画面を表示し、何か文字列を入力してボタンを押すと
ペイントを起動してその文字列を描画する、というプログラムを作ります。
それにより「大石泉すき」を画面に表示させることを今回の目的にします。

【イメージ】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541454/67010e45-91e9-9184-7578-e9f63580fbe6.png)

プログラムの大まかな流れとしては、ボタンを押した時に以下の2つの処理を行わせます。

- キーボード入力を発生させるWindow API関数を使用し、ペイントの起動と設定を行う
- マウス入力を発生させるWindows API関数を使用し、ペイント上で文字の描画を行う

なお、ペイントには画像にテキストを挿入する機能がありますが、それは使わずに
「鉛筆」ツールのみを使って文字を描かせるようにします。

#環境など
OS　： Windows 10 Pro
言語 ： VB.NET (.NET Framework 4.7.2)
開発環境：Visual Studio Community 2019

#プログラムの内容

##1. キーボード入力・マウス入力を発生させるクラス
今回作るプログラムでは、キーボード入力やマウスクリックを発生させたい箇所がいくつか出てくるので、
それらの入力を発生させる処理をまとめたクラスを最初に作っておきます。
キーボード入力の発生にはAPIのkeybd_event関数を、マウスクリックの発生にはmouse_event関数をそれぞれ使用します。

```vb:InputEvent.vb
Public Class InputEvent
    ' Windows APIの関数を使用するための宣言
    ' keybd_event: キーボード入力のイベントを発生させるAPI関数
    Private Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal bScan As Byte, ByVal dwFlags As Integer, ByVal dwExtraInfo As Integer)
    ' mouse_event: マウス入力のイベントを発生させるAPI関数
    Private Declare Sub mouse_event Lib "user32" (ByVal dwFlags As Integer, ByVal dx As Integer, ByVal dy As Integer, ByVal dwData As Integer, ByVal dwExtraInfo As Integer)

    '================================
    '  キー入力関連処理
    '================================
    ' キーを押す
    Public Shared Sub KeyDown(ByVal key As Byte)
        keybd_event(key, 0, 0, 0)
    End Sub

    ' キーを離す
    Public Shared Sub KeyUp(ByVal key As Byte)
        keybd_event(key, 0, 2, 0)
    End Sub

    ' キーを押して離す
    Public Shared Sub KeyPress(ByVal key As Byte)
        KeyDown(key)
        KeyUp(key)
    End Sub

    ' 文字列のキー入力を行う
    Public Shared Sub InputString(ByVal str As String, ByVal wait As Integer)
        ' 文字列全体を大文字に変換し、1文字ずつループ処理
        For Each c As Char In str.ToUpper
            ' 文字コードに対応したキーを入力する
            KeyPress(Asc(c))
            ' 一定時間待つ
            Threading.Thread.Sleep(wait)
        Next
    End Sub

    '================================
    '  マウス入力関連処理
    '================================
    ' マウスボタンを押す
    Public Shared Sub MouseDown()
        mouse_event(&H2, 0, 0, 0, 0)
    End Sub

    ' マウスボタンを離す
    Public Shared Sub MouseUp()
        mouse_event(&H4, 0, 0, 0, 0)
    End Sub
End Class
```

##2. ペイント起動処理
1.で用意したクラスのキーボード入力処理を使って以下の(2-1)～(2-5)のキー入力を行うことにより、
ペイントの起動と初期設定を行います。

###(2-1) ペイントを起動する
**Windowsキー + Rキー** を押して「ファイル名を指定して実行」のウィンドウを表示し、
**"mspaint"を入力 > Enterキー** でペイントを起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541454/6964af5e-1f18-e592-fbbe-457a008a21ee.png)

###(2-2) ウィンドウを最大化する
ペイントを起動した後、 **Windowsキー + ↑キー** を入力してウィンドウを最大化します。

###(2-3) キャンバスサイズを設定する
**Altキー > Fキー > Eキー** を順に入力して「イメージのプロパティ」ウィンドウを表示し、
**幅を入力 > Tabキー > 高さを入力 > Enterキー** でキャンバスのサイズを設定します。
※ 幅と高さの値は設定ファイル(後述)から取得する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541454/63b1d5d9-5d99-4cf1-a451-8fad550bf726.png)

###(2-4) 「鉛筆」ツールを選択する
**Altキー > Hキー > Pキー > 1キー** の順に入力し、ペイントの「鉛筆」ツールを選択します。

###(2-5) 線の太さを設定する
**Ctrlキー + テンキーの「+」** を押すことで鉛筆の線が少し太くなるので、
それを必要なだけ繰り返して線の太さを設定します。
※ 太さの設定値は設定ファイルから取得する

##3. 文字列描画処理
以下の(3-1)～(3-2)の処理を行うことにより、テキストボックスに入力した文字列をペイント上に描画します。
マウスボタンを押す／離す処理は、1.のクラスを使用して行います。

###(3-1) スクリーン大の画像オブジェクトを用意し、そこに文字列を描画する
ペイントに描画していく前の準備として、まずスクリーンと同じサイズの画像(ビットマップ)オブジェクトを作成し、
その画像にテキストボックスに入力した文字列を描画します。
このとき、文字周辺の領域は白く塗り潰しておきます。
※ 文字フォントの種類とサイズ、および描画開始位置(X, Y)は設定ファイルから取得する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541454/71481f09-774e-8f1e-c509-0a3035ce1387.png)

###(3-2) 画像に描いた文字をスキャンし、黒い部分と同じ位置でマウスを押す
(3-1)で作った画像に対してジグザグ状にスキャンを行い、以下のようにマウスボタンを押す/離す動作を行うことで
ペイントの画面上に文字を描画していきます。

- 色が**白から黒**に変わる位置を見つけたら、画面上の同じ位置でマウスボタンを**押す**
- 色が**黒から白**に変わる位置を見つけたら、画面上の同じ位置でマウスボタンを**離す**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541454/3b71dc19-833d-1580-2836-cca69abdf97a.png)

#ソースコード
以下が実際のソースコードおよび設定ファイルの中身になります。
これらに加えて、上の方で書いたInputEventクラスを使用します。

```vb:Form1.vb
Imports System.Configuration

Public Class Form1
    ' 設定値取得用の変数宣言
    Private canvasWidth As Integer  ' キャンバスの幅
    Private canvasHeight As Integer ' キャンバスの高さ
    Private lineWeight As Integer   ' 線の太さ
    Private fontName As String      ' フォント名
    Private fontSize As Integer     ' フォントサイズ
    Private offsetX As Integer      ' 描画開始X座標
    Private offsetY As Integer      ' 描画開始Y座標
    Private scanAngle As Double     ' スキャン角度
    Private turnAngle As Double     ' スキャンを折り返す時の角度
    Private movement As Double      ' スキャンの移動量

    ' OKボタンクリック時処理
    Private Sub btnOK_Click(sender As Object, e As EventArgs) Handles btnOK.Click
        'ペイントを起動する
        StartupPaint()

        '文字を描画する
        PaintString(txtInput.Text)
    End Sub

    ' ペイント起動処理
    Private Sub StartupPaint()
        '*******************************************************************
        ' スクリーン大の画像オブジェクトを用意し、そこに文字列を描画する
        '*******************************************************************
        ' Windows + R (ファイル名を指定して実行)
        InputEvent.KeyDown(Keys.LWin)
        Threading.Thread.Sleep(50)
        InputEvent.KeyPress(Keys.R)
        Threading.Thread.Sleep(50)
        InputEvent.KeyUp(Keys.LWin)
        Threading.Thread.Sleep(500)

        ' "mspaint" > Enter (ペイントを起動)
        InputEvent.InputString("mspaint", 150)
        Threading.Thread.Sleep(200)
        InputEvent.KeyPress(Keys.Enter)
        Threading.Thread.Sleep(500)

        '*******************************************************************
        ' ウィンドウを最大化する
        '*******************************************************************
        ' Windows + ↑
        InputEvent.KeyDown(Keys.LWin)
        InputEvent.KeyPress(Keys.Up)
        InputEvent.KeyUp(Keys.LWin)
        Threading.Thread.Sleep(500)

        '*******************************************************************
        ' キャンバスサイズを設定する
        '*******************************************************************
        ' Alt > F > E > サイズ入力 > Enter
        InputEvent.KeyPress(Keys.Menu)
        InputEvent.KeyPress(Keys.F)
        InputEvent.KeyPress(Keys.E)
        Threading.Thread.Sleep(200)
        InputEvent.InputString(canvasWidth, 150)
        InputEvent.KeyPress(Keys.Tab)
        InputEvent.InputString(canvasHeight, 150)
        Threading.Thread.Sleep(200)
        InputEvent.KeyPress(Keys.Enter)
        Threading.Thread.Sleep(200)

        '*******************************************************************
        ' 「鉛筆」ツールを選択する
        '*******************************************************************
        ' Alt > H > 1
        InputEvent.KeyPress(Keys.Menu)
        InputEvent.KeyPress(Keys.H)
        InputEvent.KeyPress(Keys.P)
        InputEvent.KeyPress(Keys.D1)
        Threading.Thread.Sleep(200)

        '*******************************************************************
        ' 線の太さを設定する
        '*******************************************************************
        ' Ctrl + テンキーの「+」
        InputEvent.KeyDown(Keys.LControlKey)
        For i As Integer = 2 To lineWeight
            InputEvent.KeyPress(Keys.Add)
            Threading.Thread.Sleep(50)
        Next
        InputEvent.KeyUp(Keys.LControlKey)
        Threading.Thread.Sleep(200)
    End Sub

    ' 文字列描画処理
    Private Sub PaintString(ByVal str As String)
        '*******************************************************************
        ' スクリーン大の画像オブジェクトを用意し、そこに文字列を描画する
        '*******************************************************************
        ' スクリーンと同じ大きさの四角形領域
        Dim screenArea As Rectangle = Screen.PrimaryScreen.Bounds
        ' 文字列の描画に使うフォント
        Dim font As New Font(fontName, fontSize)

        ' 画像オブジェクトを作成する
        Dim bmp As New Bitmap(screenArea.Width, screenArea.Height)
        ' 画像に対する描画用オブジェクトを作成する
        Dim g As Graphics = Graphics.FromImage(bmp)


        '*******************************************************************
        ' 画像に描いた文字をスキャンし、黒い部分と同じ位置でマウスを押す
        '*******************************************************************
        ' 文字列周辺の領域を取得する
        Dim sz As SizeF = g.MeasureString(str, font)
        Dim drawingArea As New Rectangle(offsetX, offsetY, sz.Width, sz.Height)

        ' 文字列の周辺を白で塗り潰す
        g.FillRectangle(Brushes.White, drawingArea)
        ' 文字列を描画する
        g.DrawString(str, font, Brushes.Black, offsetX, offsetY)
        ' 描画用オブジェクトを破棄する
        g.Dispose()

        ' 下向きに移動する時の角度
        Dim downAngle As Double = (scanAngle + 90.0) * Math.PI / 180.0
        ' 上向きに移動する時の角度
        Dim upAngle As Double = (scanAngle - 90.0 + turnAngle) * Math.PI / 180.0

        ' 移動量の初期値
        Dim dx As Double = movement * Math.Cos(downAngle)
        Dim dy As Double = movement * Math.Sin(downAngle)

        ' スキャン位置の初期値
        Dim pt As Point = drawingArea.Location
        Dim x As Double = pt.X
        Dim y As Double = pt.Y

        Dim ptLast As Point = pt
        Dim isBlackLast As Boolean = False

        Do
            ' スキャン位置が描画領域内の場合、マウス操作処理を行う
            If drawingArea.Contains(pt) AndAlso screenArea.Contains(pt) Then

                ' 今回のスキャン位置の色が黒かどうかを取得する
                Dim isBlack As Boolean = bmp.GetPixel(pt.X, pt.Y).GetBrightness < 0.5

                If isBlack AndAlso Not isBlackLast Then
                    ' 今回が黒かつ前回が白の場合、今回のスキャン位置にカーソルを移動しマウスを押す
                    Cursor.Position = pt
                    InputEvent.MouseDown()
                    Threading.Thread.Sleep(10)
                ElseIf isBlackLast AndAlso Not isBlack Then
                    ' 前回が黒かつ今回が白の場合、前回のスキャン位置にカーソルを移動しマウスを離す
                    Cursor.Position = ptLast
                    InputEvent.MouseUp()
                    Threading.Thread.Sleep(10)
                End If

                ' 今回の位置と色を記憶する
                ptLast = pt
                isBlackLast = isBlack
            End If

            ' スキャン位置を移動する
            x += dx
            y += dy
            pt.X = Math.Round(x)
            pt.Y = Math.Round(y)

            ' 右端を超えた状態で下端に到達した場合、ループを終了する
            If pt.X >= drawingArea.Right AndAlso pt.Y >= drawingArea.Bottom Then
                Exit Do
            End If

            If pt.Y >= drawingArea.Bottom AndAlso dy >= 0 Then
                ' 下方向へ移動中に下端に到達した場合、移動方向を上向きに変える
                dx = movement * Math.Cos(upAngle)
                dy = movement * Math.Sin(upAngle)
            ElseIf pt.Y <= drawingArea.Top AndAlso dy < 0 Then
                ' 上方向へ移動中に上端に到達した場合、移動方向を下向きに変える
                dx = movement * Math.Cos(downAngle)
                dy = movement * Math.Sin(downAngle)
            End If
        Loop

        ' 最後にマウスを離す
        InputEvent.MouseUp()
        bmp.Dispose()
    End Sub

    ' 画面読み込み時処理
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' App.configファイルに記述した設定を読み込む
        canvasWidth = ConfigurationManager.AppSettings("canvasWidth")   ' キャンバスサイズ(幅)
        canvasHeight = ConfigurationManager.AppSettings("canvasHeight") ' キャンバスサイズ(高さ)
        lineWeight = ConfigurationManager.AppSettings("lineWeight")     ' 線の太さ
        fontName = ConfigurationManager.AppSettings("fontName")         ' フォント名
        fontSize = ConfigurationManager.AppSettings("fontSize")         ' フォントサイズ
        offsetX = ConfigurationManager.AppSettings("offsetX")           ' 描画始点(X)
        offsetY = ConfigurationManager.AppSettings("offsetY")           ' 描画始点(Y)
        scanAngle = ConfigurationManager.AppSettings("scanAngle")       ' スキャン角度
        turnAngle = ConfigurationManager.AppSettings("turnAngle")       ' スキャンを折り返す時の角度
        movement = ConfigurationManager.AppSettings("movement")         ' スキャンの移動量
    End Sub
End Class
```
```config:App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>
    <appSettings>
      <!-- キャンバスサイズ -->
      <add key="canvasWidth" value="1280"/>
      <add key="canvasHeight" value="720"/>
      <!-- 線の太さ -->
      <add key="lineWeight" value="4"/>
      <!-- フォント -->
      <add key="fontName" value="MS UI Gothic"/>
      <add key="fontSize" value="150"/>
      <!-- 描画始点 -->
      <add key="offsetX" value="100"/>
      <add key="offsetY" value="300"/>
      <!-- スキャン角度[deg] -->
      <add key="scanAngle" value="30.0"/>
      <add key="turnAngle" value="1.0"/>
      <!-- スキャン移動量 -->
      <add key="movement" value="1.0"/>
    </appSettings>
</configuration>
```

#実行結果
作成したプログラムを動かすと以下のようになります。
テキストボックスに「大石泉すき」と入力してOKボタンを押すまでが手動の操作で、
その後は全自動で動きます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">「大石泉すき」 Advent Calendar 2019 投稿用の動画です。 <a href="https://t.co/QIjU6N74X1">pic.twitter.com/QIjU6N74X1</a></p>&mdash; ルカニロイド (@mftail) <a href="https://twitter.com/mftail/status/1208033105434398720?ref_src=twsrc%5Etfw">December 20, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

「大石泉すき」と描かせることができました。めでたしめでたし。

##おまけ
![omake1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541454/0c7eafae-374a-2744-0a5d-0bd9773caf78.png)
![omake2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/541454/7a951653-b4cc-c835-4d83-f4afcfcccdea.png)