Accessのレポートの初期表示を1ページではなく指定ページで表示する

最近ではきっと少なくなっているであろうAccessです。
業務で少しはまったのでメモとして残しておきます。

表題の通りです。
Accessのレポート表示の際に、1ページ目ではなく指定ページを表示する方法です。

# 環境
・Windows10、Windows7
・Access 2010、Access runtime 2010

# 必須ツール
・Spy++（ウィンドウハンドルのクラス名を調べるのに使用します）

# 概要
今回のシステムでは、レポートの起動方法として2パターンありました。

1. レポートビュー（ポップアップなし）
2. レポートビュー（ポップアップあり）

共に起動方法は以下になります。

```
DoCmd.OpenReport "レポート名", acViewPreview
```

どちらもAccess標準ではできないことなので、WindowsAPIを使用します。


## レポートビュー（ポップアップなし）

こちらは簡単です。
レポート自体がキーボードの矢印キー（←→）でページ移動できますので、その命令を送ってあげればいいだけです。

```
SendKeys "{RIGHT}"
```

汎用的にするために、ユーティリティ関数を作って関数化してあげるといいかもしれませんね。

```vbnet:WindowsUtility（標準モジュール）
Public Function ReportMoveNextPage()
    SendKeys "{RIGHT}"
End Function
```

レポートを起動する部分はこうなります。
移動したいページ分ReportMoveNextPageを実行してあげればよいです。
ただ、移動ページ数が多い場合は面倒なのでポップアップありの方法をとった方がいいかもしれません。

```vbnet:レポート起動元オブジェクト
DoCmd.OpenReport "レポート名", acViewPreview
WindowsUtility.ReportMoveNextPage
```

今回の場合は2ページ目を表示したいだけだったので、以下のようにしました。

```vbnet:レポートオブジェクト
Dim firstFlag As Boolean

Private Sub Report_Load()
    firstFlag = True
End Sub

Private Sub ページフッターセクション_Format(Cancel As Integer, FormatCount As Integer)
    If firstFlag Then
        '初期表示の場合のみ2ページ目を初期表示とする
        WindowsUtility.ReportMoveNextPage
        firstFlag = False
    End If
End Sub
```


## レポートビュー（ポップアップあり）

こちらはやや手間がかかります。
矢印キーでページ移動ができないため、レポート下部のページ番号を手動で変更してあげる必要があります。

### WindowsAPIの定義
まず、使用する関数を定義します。
ポップアップなしで作成したユーティリティ関数に以下を追加します。

```vbnet:WindowsUtility（標準モジュール）
' Windowハンドルの検索
Public Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" _
    (ByVal lpClassName As String, ByVal lpWindowName As String) As Long

' 親ハンドルを指定して検索
Public Declare PtrSafe Function FindWindowEx Lib "user32.dll" Alias "FindWindowExA" _
    (ByVal hwndParent As Long, ByVal hwndChildAfter As Long, _
    ByVal lpszClass As String, ByVal lpszWindow As String) As Long

' ウィンドウタイトル取得
Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
    (ByVal hWnd&, ByVal lpString$, ByVal cch&) As Long

' ウィンドウタイトル変更
Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" _
    (ByVal hWnd As Long, ByVal lpString As String) As Long
```

### 処理関数の追加

続いて、以下の関数を追加します。
説明は後ほど。

```vbnet:WindowsUtility（標準モジュール）
Public Sub ReportMovePage()

    Dim windowHandle As Long
    windowHandle = WindowsUtility.FindWindow("OReportPopup", vbNullString)   'レポートのハンドルを取得
    
    '配下のページ番号部分のハンドルを取得
    If windowHandle <> 0 Then
        windowHandle = WindowsUtility.FindWindowEx(windowHandle, 0, "OSUI", vbNullString)
    End If
    
    If windowHandle <> 0 Then
        windowHandle = WindowsUtility.FindWindowEx(windowHandle, 0, "NetUINativeHWNDHost", vbNullString)
    End If
    
    If windowHandle <> 0 Then
        windowHandle = WindowsUtility.FindWindowEx(windowHandle, 0, "NetUIHWND", vbNullString)
    End If

    If windowHandle <> 0 Then
        '同じクラス名のウィンドウハンドルが2つ存在するので2回ループして取得する
        Dim sameLevelWindowHandle As Long
        sameLevelWindowHandle = 0
        For i = 0 To 1 Step 1
            sameLevelWindowHandle = WindowsUtility.FindWindowEx(windowHandle, sameLevelWindowHandle, "NetUICtrlNotifySink", vbNullString)
    
            If sameLevelWindowHandle <> 0 Then
                'ここでやっとページ番号部分のハンドルを取得
                Dim pageNoHandle As Long
                pageNoHandle = WindowsUtility.FindWindowEx(sameLevelWindowHandle, 0, "RICHEDIT60W", vbNullString)
                
                If pageNoHandle <> 0 Then
                    'メッセージを送信する
                    Dim strWindowText As String * 10   'キャプションを受け取る変数
                    Dim pageNoCount As Integer
                    pageNoCount = GetWindowText(pageNoHandle, strWindowText, Len(strWindowText))
                    
                    'Nullを除去した文字列とタイトルを比較する
                    If Left$(strWindowText, InStr(strWindowText, vbNullChar) - 1) = "1" Then
                        
                        'タイトルを書き換える
                        Dim result As Integer
                        result = SetWindowText(pageNoHandle, "3")
                        
                        If result <> 0 Then
                            'ページ番号の書き換えに成功
                            'ENTERキーを送信
                            Dim lngTemp1 As Long
                            Dim lngTemp2 As Long
                            Dim lngReturn As Long

                            lngTemp1 = WindowsUtility.MapVirtualKey(vbKeyReturn, 0&)
                            lngTemp2 = WindowsUtility.MakeDWord(1, CInt(lngTemp1))
                            lngReturn = WindowsUtility.PostMessage(pageNoHandle, WindowsUtility.WM_KEYDOWN, 13, lngTemp2)
                        End If
                        
                        Exit For
                    End If
               End If
            End If
        Next
    End If
End Sub
```

流れとしては簡単で、以下になります。
1. レポートのウィンドウハンドルを探す
2. 配下のページ番号表示エリアのウィンドウハンドルを探す
3. ページ番号の書き換えメッセージを送信する
4. ENTERキー押下メッセージを送信する


#### レポートのウィンドウハンドルを探す

Accessレポートのウィンドウハンドルのクラス名は`OReportPopup`です。
このウィンドウハンドルを検索します。

```vbnet:WindowsUtility（標準モジュール）
    Dim windowHandle As Long
    windowHandle = WindowsUtility.FindWindow("OReportPopup", vbNullString)   'レポートのハンドルを取得
```

#### 配下のページ番号表示エリアのウィンドウハンドルを探す

あとはひたすらページ番号表示エリアのウィンドウハンドルを探すのを繰り返します。

```vbnet:WindowsUtility（標準モジュール）
    '配下のページ番号部分のハンドルを取得
    If windowHandle <> 0 Then
        windowHandle = WindowsUtility.FindWindowEx(windowHandle, 0, "OSUI", vbNullString)
    End If
    
    If windowHandle <> 0 Then
        windowHandle = WindowsUtility.FindWindowEx(windowHandle, 0, "NetUINativeHWNDHost", vbNullString)
    End If
    
    If windowHandle <> 0 Then
        windowHandle = WindowsUtility.FindWindowEx(windowHandle, 0, "NetUIHWND", vbNullString)
    End If

    If windowHandle <> 0 Then
        '同じクラス名のウィンドウハンドルが2つ存在するので2回ループして取得する
        Dim sameLevelWindowHandle As Long
        sameLevelWindowHandle = 0
        For i = 0 To 1 Step 1
            sameLevelWindowHandle = WindowsUtility.FindWindowEx(windowHandle, sameLevelWindowHandle, "NetUICtrlNotifySink", vbNullString)
    
            If sameLevelWindowHandle <> 0 Then
                'ここでやっとページ番号部分のハンドルを取得
                Dim pageNoHandle As Long
                pageNoHandle = WindowsUtility.FindWindowEx(sameLevelWindowHandle, 0, "RICHEDIT60W", vbNullString)

-------------------------------------　省略　-------------------------------------

            End If
        Next
    End If
```

途中でループしているのは`NetUICtrlNotifySink`というクラス名のハンドルが2つあるため、どちらに存在するかチェックするためです。
また、`RICHEDIT60W`も2つ存在するため、以下のようにメッセージを送信してページ番号が1かどうか判定してチェックしています。

```vbnet:WindowsUtility（標準モジュール）
                If pageNoHandle <> 0 Then
                    'メッセージを送信する
                    Dim strWindowText As String * 10   'キャプションを受け取る変数
                    Dim pageNoCount As Integer
                    pageNoCount = GetWindowText(pageNoHandle, strWindowText, Len(strWindowText))
                    
                    'Nullを除去した文字列とタイトルを比較する
                    If Left$(strWindowText, InStr(strWindowText, vbNullChar) - 1) = "1" Then  '1ページかどうか判定

-------------------------------------　省略　-------------------------------------

                    End If
               End If
```

ここまでで対象のウィンドウハンドルが見つかったのであとは簡単です。

#### ページ番号の書き換えメッセージを送信する

ページ番号を書き換えるメッセージを送信します。
今回は3を指定しています。

```vbnet:WindowsUtility（標準モジュール）
                        'タイトルを書き換える
                        Dim result As Integer
                        result = SetWindowText(pageNoHandle, "3")
```

#### ENTERキー押下メッセージを送信する

最後にENTERキー押下メッセージを送信します。
これはポップアップなしと同じではうまくいきませんでしたので、別方法になっています。

```vbnet:WindowsUtility（標準モジュール）
                        If result <> 0 Then
                            'ページ番号の書き換えに成功
                            'ENTERキーを送信
                            Dim lngTemp1 As Long
                            Dim lngTemp2 As Long
                            Dim lngReturn As Long

                            lngTemp1 = WindowsUtility.MapVirtualKey(vbKeyReturn, 0&)
                            lngTemp2 = WindowsUtility.MakeDWord(1, CInt(lngTemp1))
                            lngReturn = WindowsUtility.PostMessage(pageNoHandle, WindowsUtility.WM_KEYDOWN, 13, lngTemp2)
                        End If
```

### レポート起動

ここまでできたところで、レポートを起動します。

```vbnet:レポート起動元オブジェクト

    DoCmd.OpenReport "レポート名", acViewPreview
    DoCmd.RunCommand acCmdZoom75   '
    DoCmd.MoveSize 1000, 500, 24000, 15000
    
    WindowsUtility.ReportMovePage
```


以上で、指定ページにレポートが切り替わります。
諸々修正してもらえれば、ポップアップなしの方もこれで実現できると思います。

# 参考

## ウィンドウハンドルって？

Windowsのウィンドウに振られている一意なIDと認識しています。
・[ウィンドウハンドル](http://www.wdic.org/w/TECH/%E3%82%A6%E3%82%A3%E3%83%B3%E3%83%89%E3%82%A6%E3%83%8F%E3%83%B3%E3%83%89%E3%83%AB)


## ウィンドウハンドルのクラス名を知るには？

結局これがわからないと今回のことは実現できません。
今回の場合、Spy++というツールを使用しました。Visual Studioの付属ツールです。
このツールを起動すると、画面上のすべてのウィンドウハンドルの情報が詳細に見ることができます。
私の場合業務でVisual Studioを使用していたため普通に使用できましたが、無料で使用できるかどうかまでは不明です。
検索等で入手方法を調べていただければと思います。

・[Spy++の概要](https://msdn.microsoft.com/ja-jp/library/dd460756.aspx)