ウィンドウコントロールのスクロールバーで内部のコンテンツ領域をスクロールする - C#

ウィンドウコントロールのスクロールバーで内部のコンテンツ領域をスクロールするコードを紹介します。

概要

こちらの記事では、ウィンドウコントロールでスクロールバーを表示するコードを紹介しました。 また、こちらの記事ではウィンドウコントロールのスクロールバーの操作イベントを検出するコードを紹介しました。
この記事では、ウィンドウコントロールのスクロールバーを利用してコントロール内部のコンテンツをスクロールするコードを紹介します。

プログラム

コード

コンポーネントを作成し、以下のコードを記述します。
MyComponent.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace WindowControlScroll
{
  public partial class MyComponent : Control
  {
    // window style constants for scrollbars
    public const int WS_VSCROLL = 0x00200000;
    public const int WS_HSCROLL = 0x00100000;

    public const int WM_LBUTTONDOWN = 0x00000201;
    public const int WM_RBUTTONDOWN = 0x00000204;

    public const int WM_HSCROLL = 0x00000114;
    public const int WM_VSCROLL = 0x00000115;

    /*
     * Scroll Bar Commands
     */
    public const int SB_LINEUP = 0;
    public const int SB_LINELEFT = 0;
    public const int SB_LINEDOWN = 1;
    public const int SB_LINERIGHT = 1;
    public const int SB_PAGEUP = 2;
    public const int SB_PAGELEFT = 2;
    public const int SB_PAGEDOWN = 3;
    public const int SB_PAGERIGHT = 3;
    public const int SB_THUMBPOSITION = 4;
    public const int SB_THUMBTRACK = 5;
    public const int SB_TOP = 6;
    public const int SB_LEFT = 6;
    public const int SB_BOTTOM = 7;
    public const int SB_RIGHT = 7;
    public const int SB_ENDSCROLL = 8;

    [StructLayout(LayoutKind.Sequential)]
    struct SCROLLINFO
    {
      public uint cbSize;
      public uint fMask;
      public int nMin;
      public int nMax;
      public uint nPage;
      public int nPos;
      public int nTrackPos;
    }

    private enum ScrollBarDirection
    {
      SB_HORZ = 0,
      SB_VERT = 1,
      SB_CTL = 2,
      SB_BOTH = 3
    }

    private enum ScrollInfoMask
    {
      SIF_RANGE = 0x0001,
      SIF_PAGE = 0x0002,
      SIF_POS = 0x0004,
      SIF_DISABLENOSCROLL = 0x0008,
      SIF_TRACKPOS = 0x0010,
      SIF_ALL = SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS
      //SIF_ALL = SIF_RANGE + SIF_PAGE + SIF_POS + SIF_TRACKPOS
    }


    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO lpsi);

    [DllImport("user32.dll")]
    private static extern int SetScrollInfo(IntPtr hwnd, int fnBar, [In] ref SCROLLINFO lpsi, bool fRedraw);


    const int ScrollOffset = 2;

    public Point sc = new Point(0, 0);


    public MyComponent()
    {
      InitializeComponent();
    }

    public MyComponent(IContainer container)
    {
      container.Add(this);

      InitializeComponent();
    }

    protected override void InitLayout()
    {
      base.InitLayout();
      int tp;
      SetVScrollBar(sc.Y, (uint)ClientSize.Height, 0, (64 * 12) + ScrollOffset, out tp);
      SetHScrollBar(sc.X, (uint)ClientSize.Width, 0, (64 * 12) + ScrollOffset, out tp);
    }


    protected override CreateParams CreateParams {
      get
      {
        CreateParams cp = base.CreateParams;
        cp.Style |= WS_HSCROLL + WS_VSCROLL;

        return cp;
      }
    }

    protected override void WndProc(ref Message message)
    {
      base.WndProc(ref message);
      short lo;
      int tp;

      switch (message.Msg) {
        case WM_HSCROLL:
          lo = LoWord((long)message.WParam);
          switch (lo) {
            case SB_ENDSCROLL:
              break;
            case SB_LEFT:
              break;
            case SB_RIGHT:
              break;
            case SB_LINELEFT:
              sc.X = Math.Max(sc.X -= 8, 0);
              break;
            case SB_LINERIGHT:
              sc.X = Math.Min(sc.X += 8, (64 * 12) - ClientRectangle.Width + ScrollOffset);
              break;
            case SB_PAGELEFT:
              sc.X = Math.Max(sc.X -= 32, 0);
              break;
            case SB_PAGERIGHT:
              sc.X = Math.Min(sc.X += 32, (64 * 12) - ClientRectangle.Width + ScrollOffset);
              break;
            case SB_THUMBPOSITION:
              break;
            case SB_THUMBTRACK:
              SCROLLINFO HScrInfo = new SCROLLINFO();
              HScrInfo.fMask = (uint)ScrollInfoMask.SIF_TRACKPOS;
              GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_HORZ, ref HScrInfo);
              sc.X = Math.Min(HScrInfo.nTrackPos, (64 * 12) - ClientRectangle.Width + ScrollOffset);
              sc.X = Math.Max(sc.X, 0);
              break;
          }

          SetHScrollBar(sc.X, 32, 0, (64 * 12) - ClientRectangle.Width + ScrollOffset, out tp);
          this.Invalidate();
          break;
        case WM_VSCROLL:
          lo = LoWord((long)message.WParam);
          switch (lo) {
            case SB_BOTTOM:
              break;
            case SB_ENDSCROLL:
              break;
            case SB_LINEDOWN:
              sc.Y = Math.Min(sc.Y += 8, (64 * 12) - ClientRectangle.Height+ ScrollOffset);
              break;
            case SB_LINEUP:
              sc.Y = Math.Max(sc.Y -= 8, 0);
              break;
            case SB_PAGEDOWN:
              sc.Y = Math.Min(sc.Y += 32, (64 * 12) - ClientRectangle.Height + ScrollOffset);
              break;
            case SB_PAGEUP:
              sc.Y = Math.Max(sc.Y -= 32, 0);
              break;
            case SB_THUMBPOSITION:
              break;
            case SB_THUMBTRACK:
              SCROLLINFO VScrInfo = new SCROLLINFO();
              VScrInfo.fMask = (uint)ScrollInfoMask.SIF_TRACKPOS;
              GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref VScrInfo);
              sc.Y = VScrInfo.nTrackPos;
              break;
            case SB_TOP:
              break;
          }
          SetVScrollBar(sc.Y, (uint)ClientSize.Height, 0, (64 * 12) + ScrollOffset, out tp);
          this.Invalidate();
          break;
      }
    }

    protected override void OnResize(EventArgs e)
    {
      base.OnResize(e);
      int tp;
      SetVScrollBar(sc.Y, (uint)ClientSize.Height, 0, (64 * 12) + ScrollOffset, out tp);
      SetHScrollBar(sc.X, (uint)ClientSize.Width, 0, (64 * 12) + ScrollOffset, out tp);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
      int rWidth = 64;
      int rHeiht = 64;

      Pen p = new Pen(Color.FromArgb(0x5d, 0x86, 0xba), 1);
      SolidBrush b = new SolidBrush(Color.FromArgb(0xcd, 0xdc, 0xef));

      Font f = new Font("MS UI Gothic", 10);
      SolidBrush bs = new SolidBrush(Color.FromArgb(0x00, 0x44, 0x99));

      int idx = 0;
      for (int y = 0; y < 12; y++) {
        for (int x = 0; x < 12; x++) {
          Rectangle drawRect = new Rectangle(x * rWidth - sc.X, y * rHeiht - sc.Y, rWidth, rHeiht);
          if (e.ClipRectangle.IntersectsWith(drawRect) == true) {

            e.Graphics.FillRectangle(b, drawRect);
            e.Graphics.DrawRectangle(p, drawRect);

            e.Graphics.DrawString(Convert.ToString(idx), f, bs, new Point(x * rWidth - sc.X + 2, y * rHeiht - sc.Y + 2));
          }
          idx++;
        }
      }
    }

    protected short LoWord(long input)
    {
      return (short)((int)input & 0xFFFF);
    }

    protected short HiWord(long input)
    {
      return (short)((int)input >> 16);
    }

    public void SetVScrollBar(int Position, uint Page, int nMin, int nMax, out int TrackPosition)
    {
      SCROLLINFO VScrInfo = new SCROLLINFO();
      GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref VScrInfo);
      VScrInfo.cbSize = (uint)Marshal.SizeOf(VScrInfo);
      VScrInfo.nPos = Position;
      VScrInfo.nPage = Page;
      VScrInfo.nMin = nMin;
      VScrInfo.nMax = nMax;
      TrackPosition = VScrInfo.nTrackPos;

      VScrInfo.fMask = (int)ScrollInfoMask.SIF_POS + (int)ScrollInfoMask.SIF_PAGE + (int)ScrollInfoMask.SIF_RANGE + (int)ScrollInfoMask.SIF_DISABLENOSCROLL;
      SetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref VScrInfo, true);
    }

    public void SetHScrollBar(int Position, uint Page, int nMin, int nMax, out int TrackPosition)
    {
      SCROLLINFO HScrInfo = new SCROLLINFO();
      GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref HScrInfo);
      HScrInfo.cbSize = (uint)Marshal.SizeOf(HScrInfo);
      HScrInfo.nPos = Position;
      HScrInfo.nPage = Page;
      HScrInfo.nMin = nMin;
      HScrInfo.nMax = nMax;
      TrackPosition = HScrInfo.nTrackPos;

      HScrInfo.fMask = (int)ScrollInfoMask.SIF_POS + (int)ScrollInfoMask.SIF_PAGE + (int)ScrollInfoMask.SIF_RANGE + (int)ScrollInfoMask.SIF_DISABLENOSCROLL;
      SetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_HORZ, ref HScrInfo, true);
    }

  }
}

UI

下図のフォームを作成します。作成したウィンドウコントロールのコンポーネントをフォームに配置します。 コントロールのAnchorプロパティをTop, Bottom, Left, Right に設定し、フォームのサイズに合わせてコントロールの大きさが変化する状態に設定します。

解説

Pointクラスのsc変数にスクロール量を保存します。
  public Point sc = new Point(0, 0);

CreateParamsプロパティをオーバーライドし、Styleの値にWS_HSCROLL + WS_VSCROLLを指定しスクロールバーを表示します。 詳細はこちらの記事を参照してください。
    protected override CreateParams CreateParams {
      get
      {
        CreateParams cp = base.CreateParams;
        cp.Style |= WS_HSCROLL + WS_VSCROLL;

        return cp;
      }
    }

以下のコードでスクロールバーを設定するメソッドを実装します。
SCROLLINFO の値は、nPos にスクロールの量(sc変数)に応じた現在のスクロール位置を設定します。 nPage はクライアント領域に表示されているコンテンツの幅、高さ(ページの領域の長さ)を設定します。 今回の例では、ClientSizeの幅、高さの値を設定します。
nMin,nMaxはスクロールバーのスクロール範囲を設定します。今回は nMinは0、nMaxはコンテンツ全体
    public void SetVScrollBar(int Position, uint Page, int nMin, int nMax, out int TrackPosition)
    {
      SCROLLINFO VScrInfo = new SCROLLINFO();
      GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref VScrInfo);
      VScrInfo.cbSize = (uint)Marshal.SizeOf(VScrInfo);
      VScrInfo.nPos = Position;
      VScrInfo.nPage = Page;
      VScrInfo.nMin = nMin;
      VScrInfo.nMax = nMax;
      TrackPosition = VScrInfo.nTrackPos;

      VScrInfo.fMask = (int)ScrollInfoMask.SIF_POS + (int)ScrollInfoMask.SIF_PAGE + (int)ScrollInfoMask.SIF_RANGE + (int)ScrollInfoMask.SIF_DISABLENOSCROLL;
      SetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref VScrInfo, true);
    }

    public void SetHScrollBar(int Position, uint Page, int nMin, int nMax, out int TrackPosition)
    {
      SCROLLINFO HScrInfo = new SCROLLINFO();
      GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref HScrInfo);
      HScrInfo.cbSize = (uint)Marshal.SizeOf(HScrInfo);
      HScrInfo.nPos = Position;
      HScrInfo.nPage = Page;
      HScrInfo.nMin = nMin;
      HScrInfo.nMax = nMax;
      TrackPosition = HScrInfo.nTrackPos;

      HScrInfo.fMask = (int)ScrollInfoMask.SIF_POS + (int)ScrollInfoMask.SIF_PAGE + (int)ScrollInfoMask.SIF_RANGE + (int)ScrollInfoMask.SIF_DISABLENOSCROLL;
      SetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_HORZ, ref HScrInfo, true);
    }

InitLayoutメソッドをオーバーライドします。InitLayoutメソッド内で、スクロールバーの設定をします。
    protected override void InitLayout()
    {
      base.InitLayout();
      int tp;
      SetVScrollBar(sc.Y, (uint)ClientSize.Height, 0, (64 * 12) + ScrollOffset, out tp);
      SetHScrollBar(sc.X, (uint)ClientSize.Width, 0, (64 * 12) + ScrollOffset, out tp);
    }

WndProcメソッドをオーバーライドし、スクロールバーの操作イベントを取得します。
今回の例では、スクロールバーの上下のボタンをクリックした場合は8Pixel スクロールし、スクロールバーの内部をクリックした場合は32Pixel スクロールします。
スクロール量を設定した後、SetVScrollBar SetHScrollBar メソッドを呼び出してスクロールバーのトラックの位置を変更します。

なお、スクロール位置がマイナスにならないよう、スクロール位置が減算される場合は、Maxメソッドで、スクロール位置が0より小さくなる場合はスクロール位置を0にする処理を追加しています。 また、スクロール位置がコンテンツからクライアント領域を引いた幅や高さ(64 * 12) - ClientRectangle.(Width または Height) + ScrollOffset より大きくなる場合は、Minメソッドを使用して、 scの値をコンテンツからクライアント領域を引いた幅や高さを超えないよう設定します。
    protected override void WndProc(ref Message message)
    {
      base.WndProc(ref message);
      short lo;
      int tp;

      switch (message.Msg) {
        case WM_HSCROLL:
          lo = LoWord((long)message.WParam);
          switch (lo) {
            case SB_ENDSCROLL:
              break;
            case SB_LEFT:
              break;
            case SB_RIGHT:
              break;
            case SB_LINELEFT:
              sc.X = Math.Max(sc.X -= 8, 0);
              break;
            case SB_LINERIGHT:
              sc.X = Math.Min(sc.X += 8, (64 * 12) - ClientRectangle.Width + ScrollOffset);
              break;
            case SB_PAGELEFT:
              sc.X = Math.Max(sc.X -= 32, 0);
              break;
            case SB_PAGERIGHT:
              sc.X = Math.Min(sc.X += 32, (64 * 12) - ClientRectangle.Width + ScrollOffset);
              break;
            case SB_THUMBPOSITION:
              break;
            case SB_THUMBTRACK:
              SCROLLINFO HScrInfo = new SCROLLINFO();
              HScrInfo.fMask = (uint)ScrollInfoMask.SIF_TRACKPOS;
              GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_HORZ, ref HScrInfo);
              sc.X = HScrInfo.nTrackPos;
              break;
          }

          SetHScrollBar(sc.X, (uint)ClientSize.Width, 0, (64 * 12) + ScrollOffset, out tp);
          this.Invalidate();
          break;
        case WM_VSCROLL:
          lo = LoWord((long)message.WParam);
          switch (lo) {
            case SB_BOTTOM:
              break;
            case SB_ENDSCROLL:
              break;
            case SB_LINEDOWN:
              sc.Y = Math.Min(sc.Y += 8, (64 * 12) - ClientRectangle.Height+ ScrollOffset);
              break;
            case SB_LINEUP:
              sc.Y = Math.Max(sc.Y -= 8, 0);
              break;
            case SB_PAGEDOWN:
              sc.Y = Math.Min(sc.Y += 32, (64 * 12) - ClientRectangle.Height + ScrollOffset);
              break;
            case SB_PAGEUP:
              sc.Y = Math.Max(sc.Y -= 32, 0);
              break;
            case SB_THUMBPOSITION:
              break;
            case SB_THUMBTRACK:
              SCROLLINFO VScrInfo = new SCROLLINFO();
              VScrInfo.fMask = (uint)ScrollInfoMask.SIF_TRACKPOS;
              GetScrollInfo(this.Handle, (int)ScrollBarDirection.SB_VERT, ref VScrInfo);
              sc.Y = VScrInfo.nTrackPos;
              break;
            case SB_TOP:
              break;
          }
          SetVScrollBar(sc.Y, (uint)ClientSize.Height, 0, (64 * 12) + ScrollOffset, out tp);
          this.Invalidate();
          break;
      }
    }

フォームをリサイズするとMyComponentコントロールのサイズも変更されるため、 SetVScrollBar SetHScrollBar メソッドを呼び出してスクロールバーを設定します。
    protected override void OnResize(EventArgs e)
    {
      base.OnResize(e);
      int tp;
      SetVScrollBar(sc.Y, (uint)ClientSize.Height, 0, (64 * 12) + ScrollOffset, out tp);
      SetHScrollBar(sc.X, (uint)ClientSize.Width, 0, (64 * 12) + ScrollOffset, out tp);
    }

描画はOnPaintメソッドに実装します。64x64ピクセルのマスを縦方向に12個、横方向に12個描画します。スクロール量のscの値スライドして描画します。
    protected override void OnPaint(PaintEventArgs e)
    {
      int rWidth = 64;
      int rHeiht = 64;

      Pen p = new Pen(Color.FromArgb(0x5d, 0x86, 0xba), 1);
      SolidBrush b = new SolidBrush(Color.FromArgb(0xcd, 0xdc, 0xef));

      Font f = new Font("MS UI Gothic", 10);
      SolidBrush bs = new SolidBrush(Color.FromArgb(0x00, 0x44, 0x99));

      int idx = 0;
      for (int y = 0; y < 12; y++) {
        for (int x = 0; x < 12; x++) {
          Rectangle drawRect = new Rectangle(x * rWidth - sc.X, y * rHeiht - sc.Y, rWidth, rHeiht);
          if (e.ClipRectangle.IntersectsWith(drawRect) == true) {

            e.Graphics.FillRectangle(b, drawRect);
            e.Graphics.DrawRectangle(p, drawRect);

            e.Graphics.DrawString(Convert.ToString(idx), f, bs, new Point(x * rWidth - sc.X + 2, y * rHeiht - sc.Y + 2));
          }
          idx++;
        }
      }
    }

実行結果

プロジェクトを実行します。下図のウィドウが表示されます。


縦スクロールバーの端のボタンをクリックします。クリックすると、縦方向に8ピクセルづつスクロールします。


横スクロールバーの端のボタンをクリックします。クリックすると、横方法に8ピクセルずつスクロールします。

スクロールバーの内部をクリックするとページのスクロールになり、32ピクセルずつスクロールします。


ウィンドウサイズをリサイズして大きくします。 ウィンドウサイズを変更するとスクロールバーのスライダーのサイズがクライアント領域のサイズに合わせて変化し、スライダーのサイズが大きくなります。


横方向に広げます。コンテンツがすべて表示される状態になると、横スクロールバーがグレーアウトして無効になります。


縦方向に広げすべてのコンテンツが表示される状態になると、縦スクロールバーもグレーアウトし無効になります。


ウィンドウサイズを小さくした場合の表示結果です。スクロールバーのスライダーのサイズが小さくなります。


動画での実行結果です。

補足
動画の実行結果を見ると、内部のコンテンツのちらつきがあることがわかります。これは、スクロール時にコントロール全体を再描画しているためです。 ちらつきを抑える場合には、スクロールインする部分のみを再描画します。 コードについてはこちらの記事を参照してください。
補足
実行結果の動作を見ると、内部のコンテンツのちらつきがあることがわかります。これは、スクロール時にコントロール全体を再描画しているためです。 ちらつきを抑える場合には、スクロールインする部分のみを再描画します。 コードについてはこちらの記事を参照してください。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
最終更新日: 2024-02-11
作成日: 2023-02-12
iPentec all rights reserverd.