WinForms PrintPreviewControl ドラッグしてスクロールの実装方法

# やりたい事
WinFormsで、PrintDocumentをPreviewするControlがあるじゃないですか。
あれ、地味に使いにくいですよね。何が使いにくいかって、拡大してはみ出してしまった時、
マウスでドラッグしてスクロールしたいじゃないですか。でも標準じゃそんな操作出来ないんですよ。
自力で実装するしかないのですが、本記事では簡単な実装方法を紹介します。

どっかのサイトで紹介記事を書いた記憶があるのですが、昔の事なので覚えてません。
また、この実装方法にどうやってたどり着いたのかも覚えていません。しかも10年以上前のお話です。
ですが、最新の.NET6.0で実装した所、ちゃんと動作したので、まだまだ使えるかなという事で紹介します。


# 解決方法
やり方は色々あると思います。ですがとある事実を知ってしまい、僕はこれが正攻法なのではと思うようになりました。
実は、PrintPreviewControlって、隠蔽化されてはいますが、内部に指定位置までスクロールさせる為の処理が存在します。
リフレクションを使いそれを呼び出せば、後はいい感じにやってくれるっていう実装方法です。
今回は、PrintPreviewControlを継承したクラスを作り、その中に実装する例を示します。

環境は下記の通りです。

Windows 10 Pro 64bit 21H1
Microsoft Visual Studio Community 2022

10年以上前から実装できたので、.NETFramework4.0系や、.NETCore、.NET全てで動作すると思います。
null警告とかは基本無視です。

## 1 PrintPreviewControlを継承したクラスを作成
```C#:C#
using System.Reflection;

 public class PrintPreviewControlEx:PrintPreviewControl
    {
        private Point oldPosition;//前回マウスポジ
        private Point currentPosition;//現在の位置
        private FieldInfo finfo_Position;//現在位置の取得用
        private MethodInfo minfo_SetPositionNoInvalidate;//魔法のスクロール処理

        public PrintPreviewControlEx()
        {
            //リフレクションを利用する為にTypeを取得する
            var _type = typeof(PrintPreviewControl);
            //隠蔽可された位置フィールド
            finfo_Position = _type.GetField("position", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.ExactBinding);
            //隠蔽可されたスクロールメソッド
            minfo_SetPositionNoInvalidate = _type.GetMethod("SetPositionNoInvalidate", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.ExactBinding);
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);
            //ドラッグ中だった場合
            if (e.Button == MouseButtons.Left)
            {
                //スクロール処理してみる 
                minfo_SetPositionNoInvalidate?.Invoke(this, new object[] { currentPosition + ((Size)(oldPosition - ((Size)e.Location))) });
            }
            else
            {
                //位置などを取っておく
                oldPosition = e.Location;
                currentPosition = (Point)finfo_Position?.GetValue(this);
            }

        }


        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);
            //OnMouseWheelを検知する為に選択状態にする
            this.Select();
        }

        //こういった操作感が欲しい人は、もちろんマウスホイールでズーム率変えたいよね！
        protected override void OnMouseWheel(MouseEventArgs e)
        {
            base.OnMouseWheel(e);
            //スクロールでズーム値変更する
            //増減量・方向はお好みで
            if (e.Delta > 0)
            {
                this.Zoom -= this.Zoom * 0.1;
            }
            else
            {
                this.Zoom += this.Zoom * 0.1;
            }

            //外部通知用にイベントを呼ぶ
            this.ZoomChanged?.Invoke(this, EventArgs.Empty);
        }

        //外部通知用のイベントハンドラを用意しておく。使わないなら上記と共に消して構わん。
        public event EventHandler<EventArgs>? ZoomChanged;
    }
```

```VisualBasic:VisualBasic

Imports System.Reflection

Public Class PrintPreviewControlEx
    Inherits PrintPreviewControl

    Private oldPosition As Point
    Private currentPosition As Point
    Private finfo_Position As FieldInfo
    Private minfo_SetPositionNoInvalidate As MethodInfo

    Sub New()
        Dim _type As Type = GetType(PrintPreviewControl)
        finfo_Position = _type.GetField("position", BindingFlags.Instance Or BindingFlags.NonPublic Or BindingFlags.ExactBinding)
        minfo_SetPositionNoInvalidate = _type.GetMethod("SetPositionNoInvalidate", BindingFlags.Instance Or BindingFlags.NonPublic Or BindingFlags.ExactBinding)
    End Sub

    Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
        MyBase.OnMouseMove(e)
        If e.Button = MouseButtons.Left Then
            minfo_SetPositionNoInvalidate.Invoke(Me, New Object() {currentPosition + (oldPosition - e.Location)})
        Else
            oldPosition = e.Location
            currentPosition = finfo_Position.GetValue(Me)
        End If
    End Sub

    Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
        MyBase.OnMouseDown(e)
        Me.Select()
    End Sub

    Protected Overrides Sub OnMouseWheel(e As MouseEventArgs)
        MyBase.OnMouseWheel(e)
        If e.Delta > 0 Then
            Me.Zoom -= Me.Zoom * 0.1
        Else
            Me.Zoom += Me.Zoom * 0.1
        End If

        RaiseEvent ZoomChanged(Me, EventArgs.Empty)
    End Sub

    Public Event ZoomChanged(sender As Object, e As EventArgs)

End Class
```

## 2 一度ビルドしてFormに張り付けて、適当にPrintDocumentを渡し、実行する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464438/fa6af53f-0d98-9b4b-0134-e52784831079.png)
こんな感じですね。
これで実行すると、ドキュメントのサイズがはみ出していた場合、ドラッグ操作でスクロールできます。
サイズが収まっている場合であっても、特にエラートラップをしなくても
エラー吐いたりしませんので、気軽です。

ついでにマウスホイールでズーム率を変えることが出来きますので、
コピペすればすぐに効果が試せます。

# あとがき
個人的には、なんでこの方法にたどり着いたのか、全然覚えてないのでモヤっとしています。
Windowsの機能で、ドラッグ操作できるプレビュー画面を見た事があり、
.NETは基本的にWinAPIの使いまわしみたいなもんだからって深堀したら出てきた。
みたいな感じだったのかな。いやー昔すぎてほんとに覚えてないです。

とにかくこれでドラッグ操作でスクロール出来るPrintPreviewControlの完成です。
わざわざ隠蔽化されてるので、何か非推奨な理由でもあるのでしょうか。
わからないのですが、この実装で10年以上困った事は一度もない為、大丈夫でしょう。

活用できそうならば、活用してみてください。